From 414f47aff86f55e310950e34c2545406264e8e1f Mon Sep 17 00:00:00 2001 From: Pedro Costa Date: Wed, 30 Mar 2022 15:54:52 -0300 Subject: [PATCH] Update ankihub client and config (#11) Changes after end-to-end testing with the api are expected. Co-authored-by: Andrew Sanchez --- .pre-commit-config.yaml | 6 +- ankihub/ankihub_client.py | 137 +++++++++++----- ankihub/config.json | 8 +- ankihub/config.py | 57 +++++-- ankihub/constants.py | 5 + ankihub/gui/editor.py | 27 +++- ankihub/gui/menu.py | 13 +- ankihub/gui/tests/test_editor.py | 28 +++- ankihub/meta.json | 2 +- ankihub/register_decks.py | 9 +- ankihub/tests/test_ankihub_client.py | 231 +++++++++++++++++++++++++++ ankihub/tests/test_register_decks.py | 13 +- ankihub/user_files/README.md | 5 + conftest.py | 1 - requirements/dev.txt | 1 + 15 files changed, 449 insertions(+), 94 deletions(-) create mode 100644 ankihub/tests/test_ankihub_client.py create mode 100644 ankihub/user_files/README.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87bc21f..d7e8a9a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: check-yaml - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black @@ -16,6 +16,7 @@ repos: rev: 5.10.1 hooks: - id: isort + args: ["--diff"] - repo: https://github.com/pycqa/flake8 rev: 4.0.1 @@ -23,6 +24,7 @@ repos: - id: flake8 - repo: https://github.com/hadialqattan/pycln - rev: v1.2.0 + rev: v1.2.5 hooks: - id: pycln + additional_dependencies: ["click==8.0.4"] diff --git a/ankihub/ankihub_client.py b/ankihub/ankihub_client.py index e4db641..5845e4e 100644 --- a/ankihub/ankihub_client.py +++ b/ankihub/ankihub_client.py @@ -1,55 +1,114 @@ -import json +from typing import Union, Dict, List import requests from ankihub.config import Config +from ankihub.constants import API_URL_BASE +from aqt.utils import showText +from requests import Response, HTTPError class AnkiHubClient: """Client for interacting with the AnkiHub API.""" def __init__(self): - self.base_url = "http://localhost:8000/" - self.config = Config() - if self.config.is_authenticated(): - self.headers = { - "Content-Type": "application/json", - "Authorization": "Token " + self.config.token, - } - else: - self.headers = {"Content-Type": "application/json"} - - def authenticate_user(self, url: str, data: dict) -> str: - """Authenticate the user and return their token.""" - token = "" - response = requests.post( - self.base_url + url, - headers={"Content-Type": "application/json"}, - data=json.dumps(data), + self._headers = {"Content-Type": "application/json"} + self._config = Config() + self._base_url = API_URL_BASE + token = self._config.get_token() + if token: + self._headers["Authorization"] = f"Token {token}" + + def _call_api(self, method, endpoint, data=None, params=None): + response = requests.request( + method=method, + headers=self._headers, + url=f"{self._base_url}{endpoint}", + json=data, + params=params, ) - if response.status_code == 200: - token = json.loads(response.content)["token"] - self.config.write_token(token) - return token - - def post_apkg(self, url, data, file): - headers = {"Authorization": "Token " + self.config.token} - return requests.post( - self.base_url + url, - headers=headers, - files={"file": open(file, "rb")}, - data=data, + try: + response.raise_for_status() + except HTTPError: + # TODO Add retry logic and log to Sentry. + showText("There was an issue with your request. Please try again.") + return response + + def login(self, credentials: dict): + response = self._call_api("POST", "/login/", credentials) + token = response.json().get("token") + if token: + self._config.save_token(token) + self._headers["Authorization"] = f"Token {token}" + self._config.save_user_email(credentials["username"]) + return response + + def signout(self): + self._config.save_token("") + self._headers["Authorization"] = "" + + def upload_deck(self, key: str) -> Response: + response = self._call_api("POST", "/decks/", data={"key": key}) + return response + + def get_deck_updates(self, deck_id: str) -> Union[Response, dict]: + response = self._call_api( + "GET", + f"/decks/{deck_id}/updates", + params={"since": f"{self._config.get_last_sync()}"}, ) + if response.status_code == 200: + self._config.save_last_sync() + return response.json() + else: + return response - def post(self, url, data): - return requests.post( - self.base_url + url, headers=self.headers, data=json.dumps(data) + def get_deck_by_id(self, deck_id: str) -> Union[Response, dict]: + response = self._call_api( + "GET", + f"/decks/{deck_id}/", ) + if response.status_code == 200: + return response.json() + else: + return response - def get(self, url): - return requests.get(self.base_url + url, headers=self.headers) + def get_note_by_anki_id(self, anki_id: str) -> Union[Response, dict]: + response = self._call_api("GET", f"/notes/{anki_id}") + if response.status_code == 200: + return response.json() + else: + return response - def submit_change(self): - print("Submitting change") + def create_change_note_suggestion( + self, + ankihub_id: str, + fields: List[Dict], + tags: List[str], + ) -> Response: + suggestion = { + "ankihub_id": ankihub_id, + "fields": fields, + "tags": tags, + } + response = self._call_api( + "POST", f"/notes/{ankihub_id}/suggestion/", suggestion + ) + return response - def submit_new_note(self): - print("Submitting new note") + def create_new_note_suggestion( + self, + deck_id: int, + ankihub_id: str, + fields: Dict[str, str], + tags: List[str], + ) -> Response: + suggestion = { + "related_deck": deck_id, + "ankihub_id": ankihub_id, + "fields": fields, + "tags": tags, + } + response = self._call_api( + "POST", f"/decks/{deck_id}/note-suggestion/", suggestion + ) + return response diff --git a/ankihub/config.json b/ankihub/config.json index 1ad9be2..c1d2a13 100644 --- a/ankihub/config.json +++ b/ankihub/config.json @@ -1,7 +1 @@ -{ - "user": - { - "token": "" - }, - "hotkey": "Alt+u" -} +{"hotkey": "Alt+u"} diff --git a/ankihub/config.py b/ankihub/config.py index 82713e7..31d2597 100644 --- a/ankihub/config.py +++ b/ankihub/config.py @@ -1,20 +1,53 @@ +import json +import os +from datetime import datetime, timezone + from aqt import mw +from .constants import TOKEN_SLUG, LAST_SYNC_SLUG, USER_EMAIL_SLUG + class Config: def __init__(self): - self.config = mw.addonManager.getConfig("ankihub") - self.user = self.config.get("user") - self.token = self.user.get("token") + # self._public_config is editable by the user. + self.public_config = mw.addonManager.getConfig("ankihub") + # This is the location for private config which is only managed by our code + # and is not exposed to the user. + # See https://addon-docs.ankiweb.net/addon-config.html#user-files + user_files_path = os.path.join( + mw.addonManager.addonsFolder("ankihub"), "user_files" + ) + self._private_config_file_path = os.path.join( + user_files_path, "private_config.json" + ) + if not os.path.exists(self._private_config_file_path): + self.private_config = {} + with open(self._private_config_file_path, "w") as f: + f.write(json.dumps(self.private_config)) + else: + with open(self._private_config_file_path) as f: + self.private_config = json.load(f) + + def _update_private_config(self, config_data: dict): + with open(self._private_config_file_path, "w") as f: + f.write(json.dumps(config_data)) + + def save_token(self, token: str): + self.private_config[TOKEN_SLUG] = token + self._update_private_config(self.private_config) + + def save_user_email(self, user_email: str): + self.private_config[USER_EMAIL_SLUG] = user_email + self._update_private_config(self.private_config) - def is_authenticated(self) -> bool: - return True if self.token else False + def get_token(self) -> str: + return self.private_config.get(TOKEN_SLUG) - def signout(self): - default = mw.addonManager.addonConfigDefaults(__name__) - mw.addonManager.writeConfig(__name__, default) + def save_last_sync(self): + date_object = datetime.now(tz=timezone.utc) + date_time_str = datetime.strftime(date_object, "%Y-%m-%dT%H:%M:%S.%f%z") + self.private_config[LAST_SYNC_SLUG] = date_time_str + self._update_private_config(self.private_config) - def write_token(self, token: str) -> None: - # TODO needs test - self.token = token - mw.addonManager.writeConfig(__name__, self.config) + def get_last_sync(self) -> str: + return self.private_config.get(LAST_SYNC_SLUG) diff --git a/ankihub/constants.py b/ankihub/constants.py index 262aa02..ce5d2b3 100644 --- a/ankihub/constants.py +++ b/ankihub/constants.py @@ -2,11 +2,16 @@ from enum import Enum URL_BASE = "https://hub.ankipalace.com/" +API_URL_BASE = "http://localhost:8000/api" URL_VIEW_NOTE = URL_BASE + "note/" ANKIHUB_NOTE_TYPE_FIELD_NAME = "AnkiHub ID (hidden)" ADDON_PATH = pathlib.Path(__file__).parent.absolute() ICONS_PATH = ADDON_PATH / "icons" +LAST_SYNC_SLUG = "last_sync" +TOKEN_SLUG = "token" +USER_EMAIL_SLUG = "user_email" + class AnkiHubCommands(Enum): CHANGE = "Suggest a change" diff --git a/ankihub/gui/editor.py b/ankihub/gui/editor.py index 4bab424..f415c21 100644 --- a/ankihub/gui/editor.py +++ b/ankihub/gui/editor.py @@ -1,3 +1,5 @@ +import uuid + from anki.hooks import addHook from aqt import gui_hooks from aqt.editor import Editor @@ -15,21 +17,34 @@ def on_ankihub_button_press(editor: Editor): # The command is expected to have been set at this point already, either by # fetching the default or by selecting a command from the dropdown menu. command = editor.ankihub_command - # Get the current Note ID for passing into the request below. - # TODO This should actually get the ankihub id from the notes first field. - _ = editor.note.id + fields = editor.note.fields + tags = editor.note.tags client = AnkiHubClient() + deck_id = editor.mw.col.decks.get_current_id() if command == AnkiHubCommands.CHANGE.value: - response = client.submit_change() + ankihub_id = fields[-1] + response = client.create_change_note_suggestion( + ankihub_id=ankihub_id, + fields=fields, + tags=tags, + ) elif command == AnkiHubCommands.NEW.value: - response = client.submit_new_note() + ankihub_id = str(uuid.uuid4()) + editor.note["AnkiHub ID"] = ankihub_id + editor.mw.col.update_note(editor.note) + response = client.create_new_note_suggestion( + deck_id=deck_id, + ankihub_id=ankihub_id, + fields=fields, + tags=tags, + ) return response def setup_editor_buttons(buttons, editor: Editor): """Add buttons to Editor.""" # TODO Figure out how to test this - config = Config().config + config = Config().public_config HOTKEY = config["hotkey"] img = str(ICONS_PATH / "ankihub_button.png") button = editor.addButton( diff --git a/ankihub/gui/menu.py b/ankihub/gui/menu.py index 0717d34..d36a2d1 100644 --- a/ankihub/gui/menu.py +++ b/ankihub/gui/menu.py @@ -12,6 +12,7 @@ QVBoxLayout, QWidget, ) +from requests.exceptions import HTTPError def main_menu_setup(): @@ -44,6 +45,7 @@ def __init__(self): self.password_box = QHBoxLayout() self.password_box_label = QLabel("Password:") self.password_box_text = QLineEdit("", self) + self.password_box_text.setEchoMode(QLineEdit.Password) self.password_box_text.setMinimumWidth(300) self.password_box.addWidget(self.password_box_label) self.password_box.addWidget(self.password_box_text) @@ -89,17 +91,16 @@ def login(self): ) ankihub_client = AnkiHubClient() - token = ankihub_client.authenticate_user( - url="auth-token/", data={"username": username, "password": password} - ) - if token: + try: + ankihub_client.login( + credentials={"username": username, "password": password} + ) self.label_results.setText("You are now logged into AnkiHub.") - else: + except HTTPError: self.label_results.setText( "AnkiHub login failed. Please make sure your username and " "password are correct for AnkiHub." ) - # TODO write the token to disk to persist credentials. @classmethod def display_login(cls): diff --git a/ankihub/gui/tests/test_editor.py b/ankihub/gui/tests/test_editor.py index 362b1ad..207fbad 100644 --- a/ankihub/gui/tests/test_editor.py +++ b/ankihub/gui/tests/test_editor.py @@ -1,15 +1,18 @@ from unittest.mock import MagicMock +from ankihub.constants import API_URL_BASE from pytest_anki import AnkiSession -def test_editor(anki_session_with_addon: AnkiSession, monkeypatch): +def test_editor(anki_session_with_addon: AnkiSession, monkeypatch, requests_mock): import ankihub.gui.editor as editor from ankihub.constants import AnkiHubCommands anki_editor = editor.setup() + # Check the default command. assert anki_editor.ankihub_command == "Suggest a change" editor.on_select_command(anki_editor, AnkiHubCommands.NEW.value) + # Check that the command was updated. assert anki_editor.ankihub_command == "Suggest a new note" editor.ankihub_message_handler( (False, None), @@ -19,8 +22,21 @@ def test_editor(anki_session_with_addon: AnkiSession, monkeypatch): assert anki_editor.ankihub_command == "Suggest a change" # Patch the editor so that it has the note attribute, which it will have when # the editor is actually instantiated during an Anki Desktop session. - mock = MagicMock() - anki_editor.note = mock - mock.id = 123 - _ = editor.on_ankihub_button_press(anki_editor) - # TODO assert response + anki_editor.note = MagicMock() + anki_editor.mw = MagicMock() + anki_editor.note.id = 123 + anki_editor.note.data = { + "tags": ["test"], + "fields": [{"name": "abc", "order": 0, "value": "abc changed"}], + } + + requests_mock.post( + f"{API_URL_BASE}/notes/{anki_editor.note.id}/suggestion/", status_code=201 + ) + monkeypatch.setattr("ankihub.ankihub_client.requests", MagicMock()) + # This test is quite limited since we don't know how to run this test with a + # "real," editor, instead of the manually instantiated one above. So for + # now, this test just checks that on_ankihub_button_press runs without + # raising any errors. + response = editor.on_ankihub_button_press(anki_editor) + assert response diff --git a/ankihub/meta.json b/ankihub/meta.json index f1cf66c..1685c75 100644 --- a/ankihub/meta.json +++ b/ankihub/meta.json @@ -1 +1 @@ -{"config": {"user": {"token": ""}}} +{"config": {"hotkey": "Alt+u"}} diff --git a/ankihub/register_decks.py b/ankihub/register_decks.py index f998693..fd7bfe8 100644 --- a/ankihub/register_decks.py +++ b/ankihub/register_decks.py @@ -82,15 +82,8 @@ def upload_deck(did: int) -> None: out_dir = pathlib.Path(tempfile.mkdtemp()) out_file = str(out_dir / f"export-{deck_uuid}.apkg") exporter.exportInto(out_file) - # TODO First we need to send a request to upload the apkg to s3 - # The ankihub request just needs to post the name of the apkg - # ("export-{deck_uuid}.apkg") ankihub_client = AnkiHubClient() - response = ankihub_client.post_apkg( - "api/deck_upload/", - {"filename": deck_name}, - out_file, - ) + response = ankihub_client.upload_deck(f"{deck_name}.apkg") tooltip("Deck Uploaded to AnkiHub") return response diff --git a/ankihub/tests/test_ankihub_client.py b/ankihub/tests/test_ankihub_client.py new file mode 100644 index 0000000..f8b56c3 --- /dev/null +++ b/ankihub/tests/test_ankihub_client.py @@ -0,0 +1,231 @@ +from datetime import datetime, timedelta, timezone + +from ankihub.constants import API_URL_BASE, LAST_SYNC_SLUG +from pytest_anki import AnkiSession + +from unittest.mock import Mock + + +def test_login(anki_session_with_addon, requests_mock): + from ankihub.ankihub_client import AnkiHubClient + + credentials_data = {"username": "test", "password": "testpassword"} + + requests_mock.post(f"{API_URL_BASE}/login/", json={"token": "f4k3t0k3n"}) + client = AnkiHubClient() + client.login(credentials=credentials_data) + assert client._headers["Authorization"] == "Token f4k3t0k3n" + + +def test_signout(anki_session_with_addon: AnkiSession): + from ankihub.ankihub_client import AnkiHubClient + + client = AnkiHubClient() + client.signout() + assert client._headers["Authorization"] == "" + assert client._config.private_config["token"] == "" + + +def test_upload_deck(anki_session_with_addon: AnkiSession, requests_mock): + from ankihub.ankihub_client import AnkiHubClient + + requests_mock.post(f"{API_URL_BASE}/decks/", status_code=201) + client = AnkiHubClient() + response = client.upload_deck("test.apkg") + assert response.status_code == 201 + + +def test_upload_deck_unauthenticated( + anki_session_with_addon: AnkiSession, requests_mock, monkeypatch +): + from ankihub.ankihub_client import AnkiHubClient + + monkeypatch.setattr("ankihub.ankihub_client.showText", Mock()) + requests_mock.post(f"{API_URL_BASE}/decks/", status_code=403) + client = AnkiHubClient() + response = client.upload_deck("test.apkg") + assert response.status_code == 403 + + +def test_get_deck_updates( + anki_session_with_addon: AnkiSession, + requests_mock, +): + from ankihub.ankihub_client import AnkiHubClient + + deck_id = 1 + date_object = datetime.now(tz=timezone.utc) - timedelta(days=30) + + timestamp = date_object.timestamp() + expected_data = { + "since": timestamp, + "notes": [ + { + "deck_id": deck_id, + "note_id": 1, + "anki_id": 1, + "tags": ["New Tag"], + "fields": [{"name": "Text", "order": 0, "value": "Fake value"}], + } + ], + } + + requests_mock.get(f"{API_URL_BASE}/decks/{deck_id}/updates", json=expected_data) + client = AnkiHubClient() + conf = client._config.private_config + response = client.get_deck_updates(deck_id=deck_id) + assert response == expected_data + assert conf[LAST_SYNC_SLUG] + + +def test_get_deck_updates_unauthenticated( + anki_session_with_addon: AnkiSession, requests_mock, monkeypatch +): + from ankihub.ankihub_client import AnkiHubClient + + deck_id = 1 + + monkeypatch.setattr("ankihub.ankihub_client.showText", Mock()) + requests_mock.get(f"{API_URL_BASE}/decks/{deck_id}/updates", status_code=403) + client = AnkiHubClient() + response = client.get_deck_updates(deck_id=deck_id) + assert response.status_code == 403 + + +def test_get_deck_by_id(anki_session_with_addon: AnkiSession, requests_mock): + from ankihub.ankihub_client import AnkiHubClient + + deck_id = 1 + date_time_str = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f%z") + + expected_data = { + "id": deck_id, + "name": "test", + "owner": 1, + "anki_id": 1, + "csv_last_upload": date_time_str, + "csv_notes_url": "http://fake-csv-url.com/test.csv", + } + + requests_mock.get(f"{API_URL_BASE}/decks/{deck_id}/", json=expected_data) + client = AnkiHubClient() + response = client.get_deck_by_id(deck_id=deck_id) + assert response == expected_data + + +def test_get_deck_by_id_unauthenticated( + anki_session_with_addon: AnkiSession, requests_mock, monkeypatch +): + from ankihub.ankihub_client import AnkiHubClient + + deck_id = 1 + + requests_mock.get(f"{API_URL_BASE}/decks/{deck_id}/", status_code=403) + monkeypatch.setattr("ankihub.ankihub_client.showText", Mock()) + client = AnkiHubClient() + response = client.get_deck_by_id(deck_id=deck_id) + assert response.status_code == 403 + + +def test_get_note_by_anki_id(anki_session_with_addon: AnkiSession, requests_mock): + from ankihub.ankihub_client import AnkiHubClient + + note_anki_id = 1 + expected_data = { + "deck_id": 1, + "note_id": 1, + "anki_id": 1, + "tags": ["New Tag"], + "fields": [{"name": "Text", "order": 0, "value": "Fake value"}], + } + requests_mock.get(f"{API_URL_BASE}/notes/{note_anki_id}", json=expected_data) + client = AnkiHubClient() + response = client.get_note_by_anki_id(anki_id=note_anki_id) + assert response == expected_data + + +def test_get_note_by_anki_id_unauthenticated( + anki_session_with_addon: AnkiSession, requests_mock, monkeypatch +): + from ankihub.ankihub_client import AnkiHubClient + + note_anki_id = 1 + + requests_mock.get(f"{API_URL_BASE}/notes/{note_anki_id}", status_code=403) + monkeypatch.setattr("ankihub.ankihub_client.showText", Mock()) + client = AnkiHubClient() + response = client.get_note_by_anki_id(anki_id=note_anki_id) + assert response.status_code == 403 + + +def test_create_change_note_suggestion( + anki_session_with_addon: AnkiSession, requests_mock +): + from ankihub.ankihub_client import AnkiHubClient + + note_id = 1 + requests_mock.post(f"{API_URL_BASE}/notes/{note_id}/suggestion/", status_code=201) + client = AnkiHubClient() + response = client.create_change_note_suggestion( + ankihub_id=1, + fields=[{"name": "abc", "order": 0, "value": "abc changed"}], + tags=["test"], + ) + assert response.status_code == 201 + + +def test_create_change_note_suggestion_unauthenticated( + anki_session_with_addon: AnkiSession, requests_mock, monkeypatch +): + from ankihub.ankihub_client import AnkiHubClient + + note_id = 1 + requests_mock.post(f"{API_URL_BASE}/notes/{note_id}/suggestion/", status_code=403) + monkeypatch.setattr("ankihub.ankihub_client.showText", Mock()) + client = AnkiHubClient() + response = client.create_change_note_suggestion( + ankihub_id=1, + fields=[{"name": "abc", "order": 0, "value": "abc changed"}], + tags=["test"], + ) + assert response.status_code == 403 + + +def test_create_new_note_suggestion( + anki_session_with_addon: AnkiSession, requests_mock +): + from ankihub.ankihub_client import AnkiHubClient + + deck_id = 1 + requests_mock.post( + f"{API_URL_BASE}/decks/{deck_id}/note-suggestion/", status_code=201 + ) + client = AnkiHubClient() + response = client.create_new_note_suggestion( + deck_id=deck_id, + ankihub_id=1, + fields=[{"name": "abc", "order": 0, "value": "abc changed"}], + tags=["test"], + ) + assert response.status_code == 201 + + +def test_create_new_note_suggestion_unauthenticated( + anki_session_with_addon: AnkiSession, requests_mock, monkeypatch +): + from ankihub.ankihub_client import AnkiHubClient + + deck_id = 1 + + requests_mock.post( + f"{API_URL_BASE}/decks/{deck_id}/note-suggestion/", status_code=403 + ) + monkeypatch.setattr("ankihub.ankihub_client.showText", Mock()) + client = AnkiHubClient() + response = client.create_new_note_suggestion( + deck_id=deck_id, + ankihub_id=1, + fields=[{"name": "abc", "order": 0, "value": "abc changed"}], + tags=["test"], + ) + assert response.status_code == 403 diff --git a/ankihub/tests/test_register_decks.py b/ankihub/tests/test_register_decks.py index 2a280fa..d34c461 100644 --- a/ankihub/tests/test_register_decks.py +++ b/ankihub/tests/test_register_decks.py @@ -1,6 +1,6 @@ import copy import pathlib -from unittest.mock import Mock +from unittest.mock import Mock, patch from pytest_anki import AnkiSession @@ -71,8 +71,9 @@ def test_populate_id_fields(anki_session: AnkiSession): def test_upload_deck(anki_session_with_config: AnkiSession, monkeypatch): from ankihub.register_decks import upload_deck - anki_session = anki_session_with_config - monkeypatch.setattr("ankihub.ankihub_client.requests", Mock()) - with anki_session.profile_loaded(): - with anki_session.deck_installed(anking_deck) as deck_id: - upload_deck(deck_id) + with patch("ankihub.ankihub_client.Config"): + anki_session = anki_session_with_config + monkeypatch.setattr("ankihub.ankihub_client.requests", Mock()) + with anki_session.profile_loaded(): + with anki_session.deck_installed(anking_deck) as deck_id: + upload_deck(deck_id) diff --git a/ankihub/user_files/README.md b/ankihub/user_files/README.md new file mode 100644 index 0000000..0e1f5c0 --- /dev/null +++ b/ankihub/user_files/README.md @@ -0,0 +1,5 @@ +This directory is for storing configuration that should persist between add-on +updates. + +The contents of this directory are managed by add-on code and should not be +manually edited. diff --git a/conftest.py b/conftest.py index c96eb96..f4c9119 100644 --- a/conftest.py +++ b/conftest.py @@ -21,7 +21,6 @@ def anki_session_with_config(anki_session: AnkiSession): meta = ROOT / "ankihub" / "meta.json" with open(config) as f: config_dict = json.load(f) - config_dict["user"]["token"] = "token" with open(meta) as f: meta_dict = json.load(f) anki_session.create_addon_config( diff --git a/requirements/dev.txt b/requirements/dev.txt index bfd3d87..ef98186 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -12,3 +12,4 @@ pyqt6-qt6==6.2.3 pyqt6-webengine==6.2.1 pyqt6-webengine-qt6==6.2.2 pyqt6_sip==13.2.1 +requests-mock==1.9.3