Skip to content
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

Enqueue background tasks #151

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
58 changes: 30 additions & 28 deletions .github/workflows/automated-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ on:

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -23,32 +22,35 @@ jobs:
'3.12',
'3.13',
'3.14.0-alpha.3',
'pypy3.9',
'pypy3.10',
]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- uses: pre-commit/action@v3.0.1
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Install dependencies
run: |
uv sync --group ci
- name: Test pinned deps with unittest
run: |
uv run coverage erase
uv run coverage run -m unittest discover
- name: Check coverage
run: |
uv run coverage report --show-missing --fail-under=95
- name: Test with dependency matrix
run: |
uv run tox
- name: Test build sdist and wheel packages
run: |
uv build
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- uses: pre-commit/action@v3.0.1
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Install dependencies
run: |
uv sync --group ci
- name: Test pinned deps with unittest
run: |
uv run coverage erase
uv run coverage run -m unittest discover
- name: Check coverage
run: |
uv run coverage report --show-missing --fail-under=95
- name: Test with dependency matrix
run: |
uv run tox
env:
TOX_GH_MAJOR_MINOR: ${{ matrix.python-version }}
- name: Test build sdist and wheel packages
run: |
uv build
125 changes: 69 additions & 56 deletions drpg/api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from __future__ import annotations

import logging
from time import sleep
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast

import httpx

from drpg.types import PrepareDownloadUrlResponse
from drpg.types import DownloadUrlResponse

if TYPE_CHECKING: # pragma: no cover
from collections.abc import Iterator
Expand All @@ -22,7 +21,10 @@ class DrpgApi:

API_URL = "https://api.drivethrurpg.com/api/vBeta/"

class PrepareDownloadUrlException(Exception):
class ApiException(Exception):
pass

class PrepareDownloadUrlException(ApiException):
UNEXPECTED_RESPONSE = "Got response with unexpected schema"
REQUEST_FAILED = "Got non 2xx response"

Expand Down Expand Up @@ -52,78 +54,89 @@ def token(self) -> TokenResponse:

if resp.status_code == httpx.codes.UNAUTHORIZED:
raise AttributeError("Provided token is invalid")
if not resp.is_success:
raise self.ApiException(resp.content)

login_data: TokenResponse = resp.json()
self._client.headers["Authorization"] = login_data["token"]
return login_data

def customer_products(self, per_page: int = 50) -> Iterator[Product]:
"""List all not archived customer's products."""
def products(self, page: int = 1, per_page: int = 50) -> Iterator[Product]:
"""List products from a specified page."""
logger.debug("Yielding products page %d", page)
resp = self._client.get(
"order_products",
params={
"getChecksum": 1,
"getFilters": 0, # Official clients defaults to 1
"page": page,
"pageSize": per_page,
"library": 1,
"archived": 0,
},
)
if not resp.is_success:
raise self.ApiException(resp.content)

page = 1
yield from resp.json()

while result := self._product_page(page, per_page):
logger.debug("Yielding products page %d", page)
yield from result
page += 1
def prepare_download_url(self, product_id: int, item_id: int) -> DownloadUrlResponse:
"""
Prepare a download link and metadata for a product's item.

def prepare_download_url(self, product_id: int, item_id: int) -> PrepareDownloadUrlResponse:
"""Generate a download link and metadata for a product's item."""
Download link does not need to be ready immediately - if it's not,
run check_download_url until it's ready.
"""

task_params = {
"siteId": 10, # Magic number, probably something like storefront ID
"index": item_id,
"getChecksums": 0, # Official clients defaults to 1
"getChecksums": 1,
}
resp = self._client.get(f"order_products/{product_id}/prepare", params=task_params)

def _parse_message(resp) -> PrepareDownloadUrlResponse:
message: PrepareDownloadUrlResponse = resp.json()
if resp.is_success:
expected_keys = PrepareDownloadUrlResponse.__required_keys__
if isinstance(message, dict) and expected_keys.issubset(message.keys()):
logger.debug("Got download url for %s - %s: %s", product_id, item_id, message)
else:
logger.debug(
"Got unexpected message when getting download url for %s - %s: %s",
product_id,
item_id,
message,
)
raise self.PrepareDownloadUrlException(
self.PrepareDownloadUrlException.UNEXPECTED_RESPONSE
)
logger.debug("Got download link for: %s - %s", product_id, item_id)
return self._parse_message(resp, product_id, item_id)

def check_download_url(self, product_id: int, item_id: int) -> DownloadUrlResponse:
task_params = {
"siteId": 10, # Magic number, probably something like storefront ID
"index": item_id,
"getChecksums": 1,
}
resp = self._client.get(f"order_products/{product_id}/check", params=task_params)
logger.debug("Checked download link for: %s - %s", product_id, item_id)
return self._parse_message(resp, product_id, item_id)

def _parse_message(
self, resp: httpx.Response, product_id: int, item_id: int
) -> DownloadUrlResponse:
message: DownloadUrlResponse = resp.json()
if resp.is_success:
expected_keys = DownloadUrlResponse.__required_keys__
if isinstance(message, dict) and expected_keys.issubset(message.keys()):
logger.debug(
"Got download url for %s - %s, status='%s'",
product_id,
item_id,
message["status"],
)
else:
logger.debug(
"Could not get download link for %s - %s: %s",
"Got unexpected message when getting download url for %s - %s: %s",
product_id,
item_id,
message,
)
raise self.PrepareDownloadUrlException(
self.PrepareDownloadUrlException.REQUEST_FAILED
self.PrepareDownloadUrlException.UNEXPECTED_RESPONSE
)
return message

while (data := _parse_message(resp))["status"].startswith("Preparing"):
logger.debug("Waiting for download link for: %s - %s", product_id, item_id)
sleep(2)
resp = self._client.get(f"order_products/{product_id}/check", params=task_params)

logger.debug("Got download link for: %s - %s", product_id, item_id)
return data

def _product_page(self, page: int, per_page: int) -> list[Product]:
"""List products from a specified page."""

return self._client.get(
"order_products",
params={
"getChecksum": 1,
"getFilters": 0, # Official clients defaults to 1
"page": page,
"pageSize": per_page,
"library": 1,
"archived": 0,
},
).json()
else:
logger.debug(
"Could not get download link for %s - %s: %s",
product_id,
item_id,
message,
)
raise self.PrepareDownloadUrlException(self.PrepareDownloadUrlException.REQUEST_FAILED)
return cast(DownloadUrlResponse, message)
Loading
Loading