Skip to content

Update Moxfield API integration according to new authentication method #273

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
Dec 29, 2024
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
5 changes: 5 additions & 0 deletions .github/actions/test-backend/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ inputs:
google-drive-api-key:
description: Your Google Drive API key, required for running the database crawler
required: true
moxfield-secret:
description: Your Moxfield API secret, required for pulling data from Moxfield
required: true
runs:
using: composite
steps:
Expand All @@ -27,3 +30,5 @@ runs:
run: |
cd MPCAutofill && pytest .
shell: bash
env:
MOXFIELD_SECRET: ${{ inputs.moxfield-secret }}
1 change: 1 addition & 0 deletions .github/workflows/test-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ jobs:
- uses: ./.github/actions/test-backend
with:
google-drive-api-key: ${{ secrets.GOOGLE_DRIVE_API_KEY }}
moxfield-secret: ${{ secrets.MOXFIELD_SECRET }}
1 change: 1 addition & 0 deletions .github/workflows/test-desktop-tool.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ jobs:
- uses: ./.github/actions/test-desktop-tool
with:
google-drive-api-key: ${{ secrets.GOOGLE_DRIVE_API_KEY }}
moxfield-secret: ${{ secrets.MOXFIELD_SECRET }}
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
- uses: ./.github/actions/test-backend
with:
google-drive-api-key: ${{ secrets.GOOGLE_DRIVE_API_KEY }}
moxfield-secret: ${{ secrets.MOXFIELD_SECRET }}
test-desktop-tool:
name: Desktop tool tests
runs-on: ${{ matrix.os }}
Expand All @@ -40,6 +41,7 @@ jobs:
- uses: ./.github/actions/test-desktop-tool
with:
google-drive-api-key: ${{ secrets.GOOGLE_DRIVE_API_KEY }}
moxfield-secret: ${{ secrets.MOXFIELD_SECRET }}
test-frontend:
name: Frontend tests
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions MPCAutofill/MPCAutofill/.env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ PATREON_REFRESH=
PATREON_CLIENT=
PATREON_SECRET=
PATREON_URL=

MOXFIELD_SECRET =
4 changes: 4 additions & 0 deletions MPCAutofill/MPCAutofill/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import os
import sys
from typing import Optional

import django_stubs_ext
import environ
Expand Down Expand Up @@ -48,6 +49,9 @@
THEME = env("THEME", default="superhero")
DESCRIPTION = env("DESCRIPTION", default="")

# Integration secrets
MOXFIELD_SECRET: Optional[str] = env("MOXFIELD_SECRET", default=None)

PREPEND_WWW = env("PREPEND_WWW", default=False)

# Quick-start development settings - unsuitable for production
Expand Down
11 changes: 9 additions & 2 deletions MPCAutofill/cardpicker/integrations/game/mtg.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from typing import Any, Type
from urllib.parse import parse_qs, urlparse

import ratelimit

from django.conf import settings

from cardpicker.integrations.game.base import GameIntegration, ImportSite
from cardpicker.models import DFCPair
from cardpicker.utils import get_json_endpoint_rate_limited
Expand Down Expand Up @@ -145,12 +149,15 @@ class Moxfield(ImportSite):
def get_host_names() -> list[str]:
return ["www.moxfield.com", "moxfield.com"] # moxfield prefers www.

# Note: requests to the Moxfield API must be rate limited to one request per second.
@classmethod
@ratelimit.sleep_and_retry # type: ignore # `ratelimit` does not implement decorator typing correctly
@ratelimit.limits(calls=1, period=1) # type: ignore # `ratelimit` does not implement decorator typing correctly
def retrieve_card_list(cls, url: str) -> str:
path = urlparse(url).path
deck_id = path.split("/")[-1]
response = cls.request(
path=f"v2/decks/all/{deck_id}", netloc="api.moxfield.com", headers={"x-requested-by": "mpcautofill"}
path=f"v2/decks/all/{deck_id}", netloc="api.moxfield.com", headers={"User-Agent": settings.MOXFIELD_SECRET}
)
response_json = response.json()
card_list = ""
Expand Down Expand Up @@ -319,7 +326,7 @@ def get_import_sites(cls) -> list[Type[ImportSite]]:
Deckstats,
MagicVille,
ManaStack,
Moxfield,
*([Moxfield] if settings.MOXFIELD_SECRET else []),
MTGGoldfish,
Scryfall,
TappedOut,
Expand Down
48 changes: 47 additions & 1 deletion MPCAutofill/cardpicker/tests/test_integrations.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import time
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
from enum import Enum
from typing import Optional

import pytest
import requests_mock

from cardpicker.integrations.game.mtg import MTG
from django.conf import settings as conf_settings

from cardpicker.integrations.game.mtg import MTG, Moxfield
from cardpicker.integrations.integrations import get_configured_game_integration


Expand Down Expand Up @@ -44,6 +50,19 @@ class Decks(Enum):

# endregion

# region fixtures

@pytest.fixture()
def moxfield_secret_setter(self):
def _callable(moxfield_secret: Optional[str]):
conf_settings.MOXFIELD_SECRET = moxfield_secret

old_secret = conf_settings.MOXFIELD_SECRET
yield _callable
conf_settings.MOXFIELD_SECRET = old_secret

# endregion

# region tests

def test_get_double_faced_card_pairs(self):
Expand All @@ -58,4 +77,31 @@ def test_valid_url(self, client, django_settings, snapshot, url: str):
assert decklist
assert Counter(decklist.splitlines()) == snapshot

@pytest.mark.parametrize(
"moxfield_secret, is_moxfield_enabled",
[
(None, False),
("", False),
("lorem ipsum", True),
],
)
def test_moxfield_enabled(self, moxfield_secret_setter, moxfield_secret, is_moxfield_enabled):
moxfield_secret_setter(moxfield_secret)
import_sites = MTG.get_import_sites()
if is_moxfield_enabled:
assert Moxfield in import_sites
else:
assert Moxfield not in import_sites

def test_moxfield_rate_limit(self, monkeypatch):
with requests_mock.Mocker() as mock:
mock.get("https://api.moxfield.com/v2/decks/all/D42-or9pCk-uMi4XzRDziQ", json={})

with ThreadPoolExecutor(max_workers=3) as pool:
t0 = time.time()
pool.map(lambda _: [Moxfield.retrieve_card_list(self.Decks.MOXFIELD.value) for _ in range(2)], range(3))
t1 = time.time()
t = t1 - t0
assert t > 5 # one second between calls

# endregion
1 change: 1 addition & 0 deletions MPCAutofill/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pytest-elasticsearch~=3.0
python-dotenv~=1.0.0
ratelimit~=2.2.1
requests~=2.31.0
requests-mock~=1.12.1
sentry-sdk~=1.30.0
syrupy~=3.0
testcontainers[postgres,elasticsearch]
Expand Down
Loading