From 0732fc26bbc5297793ce9425dc63a2533d90a866 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Wed, 28 Feb 2024 15:13:33 -0500 Subject: [PATCH] Add support for `torrentcreator` endpoints --- .github/workflows/ci.yml | 12 +- README.md | 4 +- docs/source/apidoc/torrentcreator.rst | 34 +++ docs/source/introduction.rst | 6 +- pyproject.toml | 1 + src/qbittorrentapi/__init__.py | 14 ++ src/qbittorrentapi/client.py | 2 + src/qbittorrentapi/definitions.py | 1 + src/qbittorrentapi/request.py | 3 + src/qbittorrentapi/torrentcreator.py | 316 ++++++++++++++++++++++++++ tests/test_torrentcreator.py | 152 +++++++++++++ tox.ini | 40 ++-- 12 files changed, 552 insertions(+), 33 deletions(-) create mode 100644 docs/source/apidoc/torrentcreator.rst create mode 100644 src/qbittorrentapi/torrentcreator.py create mode 100644 tests/test_torrentcreator.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95b86eb4e..bc8c687b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,12 +186,12 @@ jobs: name: html-coverage-report path: ./htmlcov - #- name: Upload Coverage to Codecov - # if: contains(fromJson('["push", "pull_request"]'), github.event_name) - # uses: codecov/codecov-action@v4.0.0 - # with: - # fail_ci_if_error: true - # token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload Coverage to Codecov + if: contains(fromJson('["push", "pull_request"]'), github.event_name) + uses: codecov/codecov-action@v4.0.0 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} install-dev: ####### diff --git a/README.md b/README.md index 080f7c89f..6b14fe4e4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@
qBittorrent Web API Client -================================ +========================== Python client implementation for qBittorrent Web API -[![GitHub Workflow Status (branch)](https://img.shields.io/github/checks-status/rmartin16/qbittorrent-api/main?style=flat-square)](https://github.com/rmartin16/qbittorrent-api/actions?query=branch%3Amain) [![Codecov branch](https://img.shields.io/codecov/c/gh/rmartin16/qbittorrent-api/main?style=flat-square)](https://app.codecov.io/gh/rmartin16/qbittorrent-api) [![Coverity Scan](https://img.shields.io/coverity/scan/21227?style=flat-square)](https://scan.coverity.com/projects/rmartin16-qbittorrent-api) [![Codacy grade](https://img.shields.io/codacy/grade/ef2975376e834af1910632cb76d05832?style=flat-square)](https://app.codacy.com/gh/rmartin16/qbittorrent-api/dashboard) [![PyPI](https://img.shields.io/pypi/v/qbittorrent-api?style=flat-square)](https://pypi.org/project/qbittorrent-api/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qbittorrent-api?style=flat-square)](https://pypi.org/project/qbittorrent-api/) +[![GitHub Workflow Status (branch)](https://img.shields.io/github/checks-status/rmartin16/qbittorrent-api/main?style=flat-square)](https://github.com/rmartin16/qbittorrent-api/actions?query=branch%3Amain) [![Codecov branch](https://img.shields.io/codecov/c/gh/rmartin16/qbittorrent-api/main?style=flat-square)](https://app.codecov.io/gh/rmartin16/qbittorrent-api) [![PyPI](https://img.shields.io/pypi/v/qbittorrent-api?style=flat-square)](https://pypi.org/project/qbittorrent-api/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qbittorrent-api?style=flat-square)](https://pypi.org/project/qbittorrent-api/)
diff --git a/docs/source/apidoc/torrentcreator.rst b/docs/source/apidoc/torrentcreator.rst new file mode 100644 index 000000000..97515958f --- /dev/null +++ b/docs/source/apidoc/torrentcreator.rst @@ -0,0 +1,34 @@ +Torrent Creator +================================ + +.. autoclass:: qbittorrentapi.torrentcreator.TorrentCreatorAPIMixIn + :members: + :undoc-members: + :exclude-members: torrentcreator, torrentcreator_addTask, torrentcreator_torrentFile, torrentcreator_deleteTask + :show-inheritance: + +.. autoclass:: qbittorrentapi.torrentcreator.TorrentCreator + :members: + :undoc-members: + :exclude-members: addTask, torrentFile, deleteTask + +.. autoclass:: qbittorrentapi.torrentcreator.TorrentCreatorTaskDictionary + :members: + :undoc-members: + :show-inheritance: + :exclude-members: torrentFile, deleteTask + +.. autoclass:: qbittorrentapi.torrentcreator.TorrentCreatorTaskStatus + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: qbittorrentapi.torrentcreator.TaskStatus + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: qbittorrentapi.torrentcreator.TorrentCreatorTaskStatusList + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index 9dfb47a8d..b20b8428a 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -5,10 +5,6 @@ Introduction :target: https://github.com/rmartin16/qbittorrent-api/actions?query=branch%3Amain .. |codecov| image:: https://img.shields.io/codecov/c/gh/rmartin16/qbittorrent-api/main?style=flat-square :target: https://app.codecov.io/gh/rmartin16/qbittorrent-api -.. |coverity| image:: https://img.shields.io/coverity/scan/21227?style=flat-square - :target: https://scan.coverity.com/projects/rmartin16-qbittorrent-api -.. |codacy| image:: https://img.shields.io/codacy/grade/ef2975376e834af1910632cb76d05832?style=flat-square - :target: https://app.codacy.com/gh/rmartin16/qbittorrent-api/dashboard .. |pypi| image:: https://img.shields.io/pypi/v/qbittorrent-api?style=flat-square :target: https://pypi.org/project/qbittorrent-api/ @@ -17,7 +13,7 @@ Introduction .. |pypi downloads| image:: https://img.shields.io/pypi/dw/qbittorrent-api?color=blue&style=flat-square :target: https://pypi.org/project/qbittorrent-api/ -|github ci| |codecov| |coverity| |codacy| +|github ci| |codecov| |pypi| |pypi versions| |pypi downloads| diff --git a/pyproject.toml b/pyproject.toml index be4ef2c6b..55d21fe0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dev = [ "pre-commit ==3.6.0 ; python_version >= '3.9'", "pytest ==8.0.0", "sphinx ==7.2.6; python_version >= '3.9'", + "sphinx-autobuild ==2024.2.4 ; python_version >= '3.9'", "sphinx-copybutton ==0.5.2", "sphinxcontrib-spelling ==8.0.0", "sphinx-autodoc-typehints ==1.25.3", diff --git a/src/qbittorrentapi/__init__.py b/src/qbittorrentapi/__init__.py index 2f3dc678f..e32b26f82 100644 --- a/src/qbittorrentapi/__init__.py +++ b/src/qbittorrentapi/__init__.py @@ -62,6 +62,14 @@ SyncMainDataDictionary, SyncTorrentPeersDictionary, ) +from qbittorrentapi.torrentcreator import ( + TaskStatus, + TorrentCreator, + TorrentCreatorAPIMixIn, + TorrentCreatorTaskDictionary, + TorrentCreatorTaskStatus, + TorrentCreatorTaskStatusList, +) from qbittorrentapi.torrents import ( Tag, TagList, @@ -137,6 +145,12 @@ "SyncTorrentPeersDictionary", "Tag", "TagList", + "TaskStatus", + "TorrentCreator", + "TorrentCreatorAPIMixIn", + "TorrentCreatorTaskDictionary", + "TorrentCreatorTaskStatus", + "TorrentCreatorTaskStatusList", "TorrentCategoriesDictionary", "TorrentDictionary", "TorrentFile", diff --git a/src/qbittorrentapi/client.py b/src/qbittorrentapi/client.py index 0cd37192c..951e25400 100644 --- a/src/qbittorrentapi/client.py +++ b/src/qbittorrentapi/client.py @@ -6,6 +6,7 @@ from qbittorrentapi.rss import RSSAPIMixIn from qbittorrentapi.search import SearchAPIMixIn from qbittorrentapi.sync import SyncAPIMixIn +from qbittorrentapi.torrentcreator import TorrentCreatorAPIMixIn from qbittorrentapi.torrents import TorrentsAPIMixIn from qbittorrentapi.transfer import TransferAPIMixIn @@ -49,6 +50,7 @@ class Client( SyncAPIMixIn, TransferAPIMixIn, TorrentsAPIMixIn, + TorrentCreatorAPIMixIn, RSSAPIMixIn, SearchAPIMixIn, ): diff --git a/src/qbittorrentapi/definitions.py b/src/qbittorrentapi/definitions.py index a528f0a2c..56378fd74 100644 --- a/src/qbittorrentapi/definitions.py +++ b/src/qbittorrentapi/definitions.py @@ -58,6 +58,7 @@ class APINames(str, Enum): Sync = "sync" Transfer = "transfer" Torrents = "torrents" + TorrentCreator = "torrentcreator" RSS = "rss" Search = "search" EMPTY = "" diff --git a/src/qbittorrentapi/request.py b/src/qbittorrentapi/request.py index 09c576d8f..5dfe9985a 100644 --- a/src/qbittorrentapi/request.py +++ b/src/qbittorrentapi/request.py @@ -48,6 +48,7 @@ from qbittorrentapi.rss import RSS from qbittorrentapi.search import Search from qbittorrentapi.sync import Sync + from qbittorrentapi.torrentcreator import TorrentCreator from qbittorrentapi.torrents import TorrentCategories, Torrents, TorrentTags from qbittorrentapi.transfer import Transfer @@ -280,6 +281,7 @@ def __init__( self._torrents: Torrents | None = None self._torrent_categories: TorrentCategories | None = None self._torrent_tags: TorrentTags | None = None + self._torrentcreator: TorrentCreator | None = None self._transfer: Transfer | None = None # turn off console-printed warnings about SSL certificate issues. @@ -313,6 +315,7 @@ def _initialize_context(self) -> None: self._torrents = None self._torrent_categories = None self._torrent_tags = None + self._torrentcreator = None self._log = None self._sync = None self._rss = None diff --git a/src/qbittorrentapi/torrentcreator.py b/src/qbittorrentapi/torrentcreator.py new file mode 100644 index 000000000..8d7c7b00c --- /dev/null +++ b/src/qbittorrentapi/torrentcreator.py @@ -0,0 +1,316 @@ +from __future__ import annotations + +import enum +import os +from collections.abc import Mapping +from functools import wraps +from typing import Any, Literal, cast + +from qbittorrentapi.app import AppAPIMixIn +from qbittorrentapi.definitions import ( + APIKwargsT, + APINames, + ClientCache, + Dictionary, + JsonValueT, + List, + ListEntry, + ListInputT, +) + + +class TaskStatus(enum.Enum): + """Enumeration of possible task statuses.""" + + FAILED = "Failed" + QUEUED = "Queued" + RUNNING = "Running" + FINISHED = "Finished" + + +class TorrentCreatorTaskStatus(ListEntry): + """ + Item in :class:`TorrentCreatorTaskStatusList` + + Definition: not documented...yet + """ + + +class TorrentCreatorTaskStatusList(List[TorrentCreatorTaskStatus]): + """Response for :meth:`~TorrentCreatorAPIMixIn.torrentcreator_status`""" + + def __init__( + self, list_entries: ListInputT, client: TorrentCreatorAPIMixIn | None = None + ): + super().__init__( + list_entries, entry_class=TorrentCreatorTaskStatus, client=client + ) + + +class TorrentCreatorAPIMixIn(AppAPIMixIn): + """ + Implementation of all ``TorrentCreator`` API methods. + + :Usage: + >>> from qbittorrentapi import Client + >>> client = Client(host="localhost:8080", username="admin", password="adminadmin") + >>> task = client.torrentcreator_add_task(source_path="/path/to/data") + >>> if TaskStatus(task.status().status) == TaskStatus.FINISHED: + >>> torrent_data = task.torrent_file() + >>> task.delete() + >>> # or + >>> client.torrentcreator_delete_task(task_id=task.taskID) + """ # noqa: E501 + + @property + def torrentcreator(self) -> TorrentCreator: + """ + Allows for transparent interaction with TorrentCreator endpoints. + + See TorrentCreator class for usage. + """ + if self._torrentcreator is None: + self._torrentcreator = TorrentCreator(client=self) + return self._torrentcreator + + def torrentcreator_add_task( + self, + source_path: str | os.PathLike[Any] | None = None, + torrent_file_path: str | os.PathLike[Any] | None = None, + format: Literal["v1", "v2", "hybrid"] | None = None, + start_seeding: bool | None = None, + is_private: bool | None = None, + optimize_alignment: bool | None = None, + padded_file_size_limit: int | None = None, + piece_size: int | None = None, + comment: str | None = None, + trackers: str | list[str] | None = None, + url_seeds: str | list[str] | None = None, + **kwargs: APIKwargsT, + ) -> TorrentCreatorTaskDictionary: + """ + Add a task to create a new torrent. + + This method was introduced with qBittorrent v5.0.0 (Web API v2.10.4). + + :raises Conflict409Error: too many existing torrent creator tasks + :param source_path: source file path for torrent content + :param torrent_file_path: file path to save torrent + :param format: BitTorrent V1 or V2; defaults to "hybrid" format if None + :param start_seeding: should qBittorrent start seeding this torrent? + :param is_private: is the torrent private or not? + :param optimize_alignment: should optimized alignment be enforced for new + torrent? + :param padded_file_size_limit: size limit for padding files + :param piece_size: size of the pieces + :param comment: comment + :param trackers: list of trackers to add + :param url_seeds: list of URLs seeds to add + """ + data = { + "sourcePath": source_path, + "torrentFilePath": os.fsdecode(torrent_file_path) + if torrent_file_path + else None, + "format": format, + "private": None if is_private is None else bool(is_private), + "optimizeAlignment": None + if optimize_alignment is None + else bool(optimize_alignment), + "startSeeding": None if start_seeding is None else bool(start_seeding), + "paddedFileSizeLimit": padded_file_size_limit, + "pieceSize": piece_size, + "comment": comment, + "trackers": self._list2string(trackers), + "urlSeeds": self._list2string(url_seeds), + } + + return self._post_cast( + _name=APINames.TorrentCreator, + _method="addTask", + data=data, + response_class=TorrentCreatorTaskDictionary, + version_introduced="2.10.4", + **kwargs, + ) + + torrentcreator_addTask = torrentcreator_add_task + + def torrentcreator_status( + self, + task_id: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentCreatorTaskStatusList: + """ + Status for a torrent creation task. + + This method was introduced with qBittorrent v5.0.0 (Web API v2.10.4). + + :raises NotFound404Error: task not found + :param task_id: ID of torrent creation task + """ + data = {"taskID": task_id} + + return self._post_cast( + _name=APINames.TorrentCreator, + _method="status", + data=data, + response_class=TorrentCreatorTaskStatusList, + version_introduced="2.10.4", + **kwargs, + ) + + def torrentcreator_torrent_file( + self, + task_id: str | None = None, + **kwargs: APIKwargsT, + ) -> bytes: + """ + Retrieve torrent file for created torrent. + + This method was introduced with qBittorrent v5.0.0 (Web API v2.10.4). + + :raises NotFound404Error: task not found + :raises Conflict409Error: torrent creation is not finished or failed + :param task_id: ID of torrent creation task + """ + data = {"taskID": task_id} + + return self._post_cast( + _name=APINames.TorrentCreator, + _method="torrentFile", + data=data, + response_class=bytes, + version_introduced="2.10.4", + **kwargs, + ) + + torrentcreator_torrentFile = torrentcreator_torrent_file + + def torrentcreator_delete_task( + self, + task_id: str | None = None, + **kwargs: APIKwargsT, + ) -> None: + """ + Delete a torrent creation task. + + This method was introduced with qBittorrent v5.0.0 (Web API v2.10.4). + + :raises NotFound404Error: task not found + :param task_id: ID of torrent creation task + """ + data = {"taskID": task_id} + + self._post( + _name=APINames.TorrentCreator, + _method="deleteTask", + data=data, + version_introduced="2.10.4", + **kwargs, + ) + + torrentcreator_deleteTask = torrentcreator_delete_task + + +class TorrentCreatorTaskDictionary( + ClientCache[TorrentCreatorAPIMixIn], Dictionary[JsonValueT] +): + """Response for :meth:`~TorrentCreatorAPIMixIn.torrentcreator_add_task`""" + + def __init__(self, data: Mapping[str, JsonValueT], client: TorrentCreatorAPIMixIn): + self.task_id: str | None = cast(str, data.get("taskID", None)) + super().__init__(data=data, client=client) + + @wraps(TorrentCreatorAPIMixIn.torrentcreator_status) + def status(self, **kwargs: APIKwargsT) -> TorrentCreatorTaskStatus: + return self._client.torrentcreator_status(task_id=self.task_id, **kwargs)[0] + + @wraps(TorrentCreatorAPIMixIn.torrentcreator_torrent_file) + def torrent_file(self, **kwargs: APIKwargsT) -> bytes: + return self._client.torrentcreator_torrent_file(task_id=self.task_id, **kwargs) + + torrentFile = torrent_file + + @wraps(TorrentCreatorAPIMixIn.torrentcreator_delete_task) + def delete(self, **kwargs: APIKwargsT) -> None: + return self._client.torrentcreator_delete_task(task_id=self.task_id, **kwargs) + + +class TorrentCreator(ClientCache[TorrentCreatorAPIMixIn]): + """ + Allows interaction with ``TorrentCreator`` API endpoints. + + :Usage: + >>> from qbittorrentapi import Client + >>> client = Client(host="localhost:8080", username="admin", password="adminadmin") + >>> # this is all the same attributes that are available as named in the + >>> # endpoints or the more pythonic names in Client (with or without 'torrentcreator_' prepended) + >>> task = client.torrentcreator.add_task(source_path="/path/to/data") + >>> if TaskStatus(task.status().status) == TaskStatus.FINISHED: + >>> torrent_data = task.torrent_file() + >>> task.delete() + >>> # or + >>> client.torrentcreator.delete_task(task_id=task.taskID) + """ # noqa: E501 + + @wraps(TorrentCreatorAPIMixIn.torrentcreator_add_task) + def add_task( + self, + source_path: str | os.PathLike[Any] | None = None, + torrent_file_path: str | os.PathLike[Any] | None = None, + format: Literal["v1", "v2", "hybrid"] | None = None, + start_seeding: bool | None = None, + is_private: bool | None = None, + optimize_alignment: bool | None = None, + padded_file_size_limit: int | None = None, + piece_size: int | None = None, + comment: str | None = None, + trackers: str | list[str] | None = None, + url_seeds: str | list[str] | None = None, + **kwargs: APIKwargsT, + ) -> TorrentCreatorTaskDictionary: + return self._client.torrentcreator_add_task( + source_path=source_path, + torrent_file_path=torrent_file_path, + format=format, + start_seeding=start_seeding, + is_private=is_private, + optimize_alignment=optimize_alignment, + padded_file_size_limit=padded_file_size_limit, + piece_size=piece_size, + comment=comment, + trackers=trackers, + url_seeds=url_seeds, + **kwargs, + ) + + addTask = add_task + + @wraps(TorrentCreatorAPIMixIn.torrentcreator_status) + def status( + self, + task_id: str | None = None, + **kwargs: APIKwargsT, + ) -> TorrentCreatorTaskStatusList: + return self._client.torrentcreator_status(task_id=task_id, **kwargs) + + @wraps(TorrentCreatorAPIMixIn.torrentcreator_torrent_file) + def torrent_file( + self, + task_id: str | None = None, + **kwargs: APIKwargsT, + ) -> bytes: + return self._client.torrentcreator_torrent_file(task_id=task_id, **kwargs) + + torrentFile = torrent_file + + @wraps(TorrentCreatorAPIMixIn.torrentcreator_delete_task) + def delete_task( + self, + task_id: str | None = None, + **kwargs: APIKwargsT, + ) -> None: + self._client.torrentcreator_delete_task(task_id=task_id, **kwargs) + + deleteTask = delete_task diff --git a/tests/test_torrentcreator.py b/tests/test_torrentcreator.py new file mode 100644 index 000000000..96713a57b --- /dev/null +++ b/tests/test_torrentcreator.py @@ -0,0 +1,152 @@ +import sys +from pathlib import Path + +import pytest +from qbittorrentapi import APINames +from qbittorrentapi.exceptions import NotFound404Error +from qbittorrentapi.torrentcreator import ( + TaskStatus, + TorrentCreatorTaskDictionary, + TorrentCreatorTaskStatus, + TorrentCreatorTaskStatusList, +) + +from tests.utils import check + + +@pytest.fixture +def task(client) -> TorrentCreatorTaskDictionary: + source_path = Path(Path.cwd() / "tests/_resources/test_torrent") + source_path.mkdir(parents=True, exist_ok=True) + test_file = source_path / "test-file" + test_file.touch() + with test_file.open("w") as f: + f.write("hello world") + + yield ( + task := client.torrentcreator_add_task( + source_path=Path("/tmp/_resources/test_torrent"), start_seeding=False + ) + ) + + task.delete() + test_file.unlink() + source_path.rmdir() + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason="removeprefix not in 3.8") +def test_methods(client): + namespace = APINames.TorrentCreator + all_dotted_methods = set(dir(getattr(client, namespace))) + + for meth in [meth for meth in dir(client) if meth.startswith(f"{namespace}_")]: + assert meth.removeprefix(f"{namespace}_") in all_dotted_methods + + +@pytest.mark.skipif_before_api_version("2.10.4") +@pytest.mark.parametrize( + "add_task_func", + [ + "torrentcreator_add_task", + "torrentcreator_addTask", + "torrentcreator.add_task", + "torrentcreator.addTask", + ], +) +def test_add_task(client, add_task_func): + task = client.func(add_task_func)(source_path="/empty-dir", start_seeding=False) + + assert isinstance(task, TorrentCreatorTaskDictionary) + assert task.taskID + assert task.task_id + + +@pytest.mark.skipif_after_api_version("2.10.4") +@pytest.mark.parametrize( + "add_task_func", + [ + "torrentcreator_add_task", + "torrentcreator_addTask", + "torrentcreator.add_task", + "torrentcreator.addTask", + ], +) +def test_add_task_not_implemented(client, add_task_func): + with pytest.raises(NotImplementedError): + client.func(add_task_func)() + + +@pytest.mark.skipif_before_api_version("2.10.4") +@pytest.mark.parametrize( + "status_func", ["torrentcreator_status", "torrentcreator.status"] +) +def test_status(client, status_func, task): + assert isinstance( + client.func(status_func)(task_id=task.task_id), TorrentCreatorTaskStatusList + ) + assert isinstance( + client.func(status_func)(task_id=task.task_id)[0], TorrentCreatorTaskStatus + ) + assert isinstance(task.status(), TorrentCreatorTaskStatus) + assert TaskStatus(task.status().status) in TaskStatus + + +@pytest.mark.skipif_after_api_version("2.10.4") +@pytest.mark.parametrize( + "status_func", ["torrentcreator_status", "torrentcreator.status"] +) +def test_status_not_implemented(client, status_func): + with pytest.raises(NotImplementedError): + client.func(status_func)() + + +def test_status_enum(): + assert TaskStatus("Failed") is TaskStatus.FAILED + assert TaskStatus("Queued") is TaskStatus.QUEUED + assert TaskStatus("Running") is TaskStatus.RUNNING + assert TaskStatus("Finished") is TaskStatus.FINISHED + + +@pytest.mark.skipif_before_api_version("2.10.4") +@pytest.mark.parametrize( + "torrent_file_func", ["torrentcreator_torrent_file", "torrentcreator.torrent_file"] +) +def test_torrent_file(client, torrent_file_func, task): + check(lambda: TaskStatus(task.status().status), [TaskStatus.FINISHED]) + assert isinstance(client.func(torrent_file_func)(task_id=task.task_id), bytes) + assert isinstance(task.torrent_file(), bytes) + assert isinstance(task.torrentFile(), bytes) + + +@pytest.mark.skipif_after_api_version("2.10.4") +@pytest.mark.parametrize( + "torrent_file_func", ["torrentcreator_torrent_file", "torrentcreator.torrent_file"] +) +def test_torrent_file_not_implemented(client, torrent_file_func): + with pytest.raises(NotImplementedError): + client.func(torrent_file_func)() + + +@pytest.mark.skipif_before_api_version("2.10.4") +@pytest.mark.parametrize( + "torrent_delete_func", ["torrentcreator_delete_task", "torrentcreator.delete_task"] +) +def test_delete(client, torrent_delete_func): + task = client.torrentcreator_add_task(source_path="/empty-dir", start_seeding=False) + task.delete() + with pytest.raises(NotFound404Error): + task.status() + + task = client.torrentcreator_add_task(source_path="/empty-dir", start_seeding=False) + client.func(torrent_delete_func)(task_id=task.task_id) + with pytest.raises(NotFound404Error): + client.func(torrent_delete_func)(task_id=task.task_id) + + +@pytest.mark.skipif_after_api_version("2.10.4") +@pytest.mark.parametrize( + "torrent_delete_func", ["torrentcreator_delete_task", "torrentcreator.delete_task"] +) +def test_delete_not_implemented(client, torrent_delete_func): + with pytest.raises(NotImplementedError): + client.func(torrent_delete_func)() diff --git a/tox.ini b/tox.ini index f57b1bbba..561910ffb 100644 --- a/tox.ini +++ b/tox.ini @@ -28,33 +28,33 @@ commands = commands_post = !ci: docker stop qbt-tox-testing [docs] -source_dir = source -build_dir = _build -# -W: make warnings into errors -# --keep-going: continue on errors -# -j: run with multiple processes -# -n: nitpick mode -sphinx_args = -W --keep-going -j auto -n -# -v: verbose logging -# -E: force rebuild of environment -# -T: print traceback on error -# -a: read/parse all files -# -d: use tox's temp dir for caching -sphinx_args_extra = {[docs]sphinx_args} -v -E -T -a -d {envtmpdir}/doctrees +docs_dir = {tox_root}{/}docs +source_dir = {[docs]docs_dir}{/}source +build_dir = {[docs]docs_dir}{/}_build +# replace when Sphinx>=7.3 and Python 3.8 is dropped: +# -T => --show-traceback +# -W => --fail-on-warning +# -b => --builder +# -v => --verbose +# -a => --write-all +# -E => --fresh-env +sphinx_args = -T -W --keep-going --jobs auto -[testenv:docs{,-lint,-all,-man}] +[testenv:docs{,-lint,-all,-man,-live,-live-src}] base_python = py311 package = wheel wheel_build_env = .pkg change_dir = docs passenv = FORCE_COLOR -extras = dev +deps = -e {tox_root}[dev] commands = - !lint-!all-!man : python -m sphinx {[docs]sphinx_args} -b html ./{[docs]source_dir} {[docs]build_dir}/html - lint : python -m sphinx {[docs]sphinx_args_extra} -b linkcheck ./{[docs]source_dir} {[docs]build_dir}/links - lint : python -m sphinx {[docs]sphinx_args_extra} -b spelling ./{[docs]source_dir} {[docs]build_dir}/spell - all : python -m sphinx {[docs]sphinx_args_extra} -b html ./{[docs]source_dir} {[docs]build_dir}/html - man : python -m sphinx {[docs]sphinx_args_extra} -b man ./{[docs]source_dir} {[docs]build_dir}/man + !lint-!all-!man-!live : python -m sphinx {[docs]sphinx_args} -b html {[docs]source_dir} {[docs]build_dir}/html + lint : python -m sphinx {[docs]sphinx_args} -b linkcheck {[docs]source_dir} {[docs]build_dir}/links + lint : python -m sphinx {[docs]sphinx_args} -b spelling {[docs]source_dir} {[docs]build_dir}/spell + all : python -m sphinx {[docs]sphinx_args} -v -a -E -b html {[docs]source_dir} {[docs]build_dir}/html + man : python -m sphinx {[docs]sphinx_args} -b man {[docs]source_dir} {[docs]build_dir}/man + live-!src : sphinx-autobuild {[docs]sphinx_args} {posargs} -b html {[docs]source_dir} {[docs]build_dir}{/}live + live-src : sphinx-autobuild {[docs]sphinx_args} {posargs} -a -E --watch {tox_root}{/}src{/}qbittorrentapi -b html {[docs]source_dir} {[docs]build_dir}{/}live [testenv:package] skip_install = True