├── .gitattributes
├── .gitignore
├── .idea
├── MypyConfig.xml
├── clutch.iml
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .readthedocs.yml
├── LICENSE
├── README.rst
├── clutch
├── __init__.py
├── client.py
├── method
│ ├── __init__.py
│ ├── method.py
│ ├── misc.py
│ ├── queue.py
│ ├── session.py
│ ├── shared.py
│ └── torrent.py
├── network
│ ├── __init__.py
│ ├── connection.py
│ ├── rpc
│ │ ├── __init__.py
│ │ ├── convert.py
│ │ └── message.py
│ ├── session.py
│ └── utility.py
├── py.typed
└── schema
│ ├── __init__.py
│ ├── request
│ ├── __init__.py
│ ├── misc
│ │ ├── __init__.py
│ │ ├── bandwidth_group.py
│ │ └── port_test.py
│ ├── session
│ │ ├── __init__.py
│ │ ├── accessor.py
│ │ ├── mutator.py
│ │ └── shared.py
│ └── torrent
│ │ ├── __init__.py
│ │ ├── accessor.py
│ │ ├── add.py
│ │ └── mutator.py
│ └── user
│ ├── __init__.py
│ ├── method
│ ├── __init__.py
│ ├── misc.py
│ ├── session
│ │ ├── __init__.py
│ │ ├── accessor.py
│ │ ├── mutator.py
│ │ └── shared.py
│ ├── shared.py
│ └── torrent
│ │ ├── __init__.py
│ │ ├── accessor.py
│ │ ├── action.py
│ │ ├── add.py
│ │ ├── move.py
│ │ ├── mutator.py
│ │ ├── remove.py
│ │ └── rename.py
│ └── response
│ ├── __init__.py
│ ├── misc.py
│ ├── session
│ ├── __init__.py
│ ├── accessor.py
│ └── stats.py
│ └── torrent
│ ├── __init__.py
│ ├── accessor.py
│ ├── add.py
│ └── rename.py
├── docker
├── clutch.df
├── docker-compose.yml
├── integration-test-wait.sh
├── integration-wait.df
├── integration_resources
│ ├── client_setup.py
│ ├── data
│ │ ├── ion.txt
│ │ └── little_women
│ │ │ └── little_women.txt
│ └── watch
│ │ ├── ion.torrent
│ │ └── little_women.torrent
├── settings.json
└── transmission.df
├── docs
├── Makefile
├── make.bat
└── source
│ ├── clutch.method.rst
│ ├── clutch.network.rpc.rst
│ ├── clutch.network.rst
│ ├── clutch.rst
│ ├── clutch.schema.request.misc.rst
│ ├── clutch.schema.request.rst
│ ├── clutch.schema.request.session.rst
│ ├── clutch.schema.request.torrent.rst
│ ├── clutch.schema.rst
│ ├── clutch.schema.user.method.rst
│ ├── clutch.schema.user.method.session.rst
│ ├── clutch.schema.user.method.torrent.rst
│ ├── clutch.schema.user.response.rst
│ ├── clutch.schema.user.response.session.rst
│ ├── clutch.schema.user.response.torrent.rst
│ ├── clutch.schema.user.rst
│ ├── commands.rst
│ ├── conf.py
│ ├── examples.rst
│ ├── index.rst
│ ├── intro.rst
│ ├── modules.rst
│ ├── requirements.txt
│ └── server_config.rst
├── pyproject.toml
├── requirements.txt
├── rpc-spec.md
├── scripts
├── attach-shell.sh
├── build-images.sh
├── run-containers.sh
└── run-transmission.sh
├── tests
├── __init__.py
├── endtoend
│ ├── __init__.py
│ ├── test_accessor.py
│ ├── test_action.py
│ └── test_mutator.py
├── mock
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_bandwidth_groups.py
│ └── test_cases.py
└── unit
│ ├── __init__.py
│ ├── client
│ ├── __init__.py
│ ├── conftest.py
│ ├── session
│ │ ├── __init__.py
│ │ ├── test_accessor.py
│ │ └── test_mutator.py
│ ├── test_queue.py
│ └── torrent
│ │ ├── __init__.py
│ │ ├── test_accessor.py
│ │ ├── test_action.py
│ │ ├── test_add.py
│ │ ├── test_move.py
│ │ ├── test_mutator.py
│ │ ├── test_remove.py
│ │ └── test_rename.py
│ ├── schema
│ ├── __init__.py
│ └── user
│ │ ├── __init__.py
│ │ └── response
│ │ ├── __init__.py
│ │ └── torrent
│ │ ├── __init__.py
│ │ └── test_accessor.py
│ └── test_networking.py
└── uv.lock
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.html linguist-detectable=false
2 | *.htm linguist-detectable=false
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | # For a library or package, you might want to ignore these files since the code is
86 | # intended to run in multiple environments; otherwise, check them in:
87 | # .python-version
88 |
89 | # pipenv
90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
93 | # install all needed dependencies.
94 | #Pipfile.lock
95 |
96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
97 | __pypackages__/
98 |
99 | # Celery stuff
100 | celerybeat-schedule
101 | celerybeat.pid
102 |
103 | # SageMath parsed files
104 | *.sage.py
105 |
106 | # Environments
107 | .env
108 | .venv
109 | env/
110 | venv/
111 | ENV/
112 | env.bak/
113 | venv.bak/
114 |
115 | # Spyder project settings
116 | .spyderproject
117 | .spyproject
118 |
119 | # Rope project settings
120 | .ropeproject
121 |
122 | # mkdocs documentation
123 | /site
124 |
125 | # mypy
126 | .mypy_cache/
127 | .dmypy.json
128 | dmypy.json
129 |
130 | # Pyre type checker
131 | .pyre/
132 |
133 | # pytype static type analyzer
134 | .pytype/
135 |
136 | # vscode
137 | .vscode/
138 |
139 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
140 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
141 |
142 | # User-specific stuff
143 | .idea/**/workspace.xml
144 | .idea/**/tasks.xml
145 | .idea/**/usage.statistics.xml
146 | .idea/**/dictionaries
147 | .idea/**/shelf
148 |
149 | # Generated files
150 | .idea/**/contentModel.xml
151 |
152 | # Sensitive or high-churn files
153 | .idea/**/dataSources/
154 | .idea/**/dataSources.ids
155 | .idea/**/dataSources.local.xml
156 | .idea/**/sqlDataSources.xml
157 | .idea/**/dynamic.xml
158 | .idea/**/uiDesigner.xml
159 | .idea/**/dbnavigator.xml
160 |
161 | # Gradle
162 | .idea/**/gradle.xml
163 | .idea/**/libraries
164 |
165 | # Gradle and Maven with auto-import
166 | # When using Gradle or Maven with auto-import, you should exclude module files,
167 | # since they will be recreated, and may cause churn. Uncomment if using
168 | # auto-import.
169 | # .idea/artifacts
170 | # .idea/compiler.xml
171 | # .idea/jarRepositories.xml
172 | # .idea/modules.xml
173 | # .idea/*.iml
174 | # .idea/modules
175 | # *.iml
176 | # *.ipr
177 |
178 | # CMake
179 | cmake-build-*/
180 |
181 | # Mongo Explorer plugin
182 | .idea/**/mongoSettings.xml
183 |
184 | # File-based project format
185 | *.iws
186 |
187 | # IntelliJ
188 | out/
189 |
190 | # mpeltonen/sbt-idea plugin
191 | .idea_modules/
192 |
193 | # JIRA plugin
194 | atlassian-ide-plugin.xml
195 |
196 | # Cursive Clojure plugin
197 | .idea/replstate.xml
198 |
199 | # Crashlytics plugin (for Android Studio and IntelliJ)
200 | com_crashlytics_export_strings.xml
201 | crashlytics.properties
202 | crashlytics-build.properties
203 | fabric.properties
204 |
205 | # Editor-based Rest Client
206 | .idea/httpRequests
207 |
208 | # Android studio 3.1+ serialized cache file
209 | .idea/caches/build_file_checksums.ser
210 |
--------------------------------------------------------------------------------
/.idea/MypyConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/clutch.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | build:
9 | tools:
10 | python: "3.12"
11 | os: ubuntu-lts-latest
12 |
13 | # Build documentation in the docs/ directory with Sphinx
14 | sphinx:
15 | configuration: docs/source/conf.py
16 |
17 | # Optionally set the version of Python and requirements required to build your docs
18 | python:
19 | install:
20 | - method: pip
21 | path: .
22 | - requirements: docs/source/requirements.txt
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Michael Hadam
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Clutch
2 | ------
3 |
4 | .. image:: https://readthedocs.org/projects/clutch/badge/?version=latest
5 | :target: https://clutch.readthedocs.io/en/latest/?badge=latest
6 | :alt: Documentation badge
7 |
8 | .. image:: https://img.shields.io/pypi/v/transmission-clutch.svg
9 | :target: https://pypi.org/project/transmission-clutch
10 | :alt: PyPI badge
11 |
12 | .. image:: https://img.shields.io/pypi/dm/transmission-clutch.svg
13 | :target: https://pypistats.org/packages/transmission-clutch
14 | :alt: PyPI downloads badge
15 |
16 | Documentation
17 | =============
18 |
19 | Found here: ``_
20 |
21 | Quick start
22 | ===========
23 |
24 | Install the package:
25 |
26 | .. code-block:: console
27 |
28 | $ pip install transmission-clutch
29 |
30 | Make a client:
31 |
32 | .. code-block:: python
33 |
34 | from clutch import Client
35 | client = Client()
36 |
37 | If you find the client isn't connecting (an error will be raised), make sure you're entering the address correctly. Reference `urllib.parse.urlparse`_ for parsing rules.
38 |
39 | You can specify Transmission's address when making the client:
40 |
41 | .. code-block:: python
42 |
43 | client = Client(address="http://localhost:9091/transmission/rpc")
44 |
45 | .. _urllib.parse.urlparse: https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlparse
46 |
47 | RPC methods are separated into groups: torrent, session, queue and misc.
48 |
49 | Methods are called by first specifying a group:
50 |
51 | .. code-block:: python
52 |
53 | client.torrent.add(...)
54 |
--------------------------------------------------------------------------------
/clutch/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import Client
2 |
3 | __all__ = ["Client"]
4 |
--------------------------------------------------------------------------------
/clutch/client.py:
--------------------------------------------------------------------------------
1 | from clutch.method.misc import MiscellaneousMethods
2 | from clutch.method.queue import QueueMethods
3 | from clutch.method.session import SessionMethods
4 | from clutch.method.torrent import TorrentMethods
5 | from clutch.network.connection import Connection
6 | from clutch.network.session import TransmissionSession
7 | from clutch.network.utility import make_endpoint
8 |
9 |
10 | class Client:
11 | session: SessionMethods = SessionMethods()
12 | torrent: TorrentMethods = TorrentMethods()
13 | queue: QueueMethods = QueueMethods()
14 | misc: MiscellaneousMethods = MiscellaneousMethods()
15 |
16 | def __init__(
17 | self,
18 | address="http://localhost:9091/transmission/rpc",
19 | scheme=None,
20 | host=None,
21 | port=None,
22 | path=None,
23 | query=None,
24 | username=None,
25 | password=None,
26 | debug=False,
27 | ):
28 | self._endpoint: str = make_endpoint(address, scheme, host, port, path, query)
29 | self._session: TransmissionSession = TransmissionSession(username, password)
30 | self._connection: Connection = Connection(self._endpoint, self._session, debug)
31 |
32 | def set_rpc_debug(self, value: bool):
33 | self._connection.debug = value
34 |
35 | def set_connection(
36 | self,
37 | address="http://localhost:9091/transmission/rpc",
38 | scheme=None,
39 | host=None,
40 | port=None,
41 | path=None,
42 | query=None,
43 | username=None,
44 | password=None,
45 | debug=False,
46 | ):
47 | self._endpoint = make_endpoint(address, scheme, host, port, path, query)
48 | self._session = TransmissionSession(username, password)
49 | self._connection = Connection(self._endpoint, self._session, debug)
50 |
--------------------------------------------------------------------------------
/clutch/method/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/method/__init__.py
--------------------------------------------------------------------------------
/clutch/method/method.py:
--------------------------------------------------------------------------------
1 | from typing import TypeVar
2 | from weakref import proxy
3 |
4 | from clutch.network.connection import Connection
5 |
6 | T = TypeVar("T", bound="MethodNamespace")
7 |
8 |
9 | class MethodNamespace:
10 | _connection: Connection = ... # type: ignore
11 |
12 | def __init__(self, client=None):
13 | if client is not None:
14 | self._connection = proxy(client.__dict__["_connection"])
15 |
16 | def __get__(self: T, instance, owner) -> T:
17 | return self.__class__(instance)
18 |
--------------------------------------------------------------------------------
/clutch/method/misc.py:
--------------------------------------------------------------------------------
1 | from clutch.method.method import MethodNamespace
2 | from clutch.network.rpc.message import Request, Response
3 | from clutch.schema.request.misc.port_test import PortTestArgumentsRequest
4 | from clutch.schema.user.method.misc import IpProtocol
5 | from clutch.schema.user.response.misc import (
6 | BandwidthGroupResponse,
7 | BlocklistResponse,
8 | FreeSpaceResponse,
9 | PortTestResponse,
10 | )
11 |
12 |
13 | class MiscellaneousMethods(MethodNamespace):
14 | def blocklist_update(self, tag: int | None = None) -> Response[BlocklistResponse]:
15 | """Trigger an update of the client blocklist."""
16 | return self._connection.send(
17 | Request(method="blocklist-update", tag=tag), BlocklistResponse
18 | )
19 |
20 | def port_test(
21 | self, ip_protocol: IpProtocol | None = None, tag: int | None = None
22 | ) -> Response[PortTestResponse]:
23 | """Test the client to see if the port settings are open for connections."""
24 | return self._connection.send(
25 | Request[PortTestArgumentsRequest](
26 | method="port-test", arguments={"ip_protocol": ip_protocol}, tag=tag
27 | ),
28 | PortTestResponse,
29 | )
30 |
31 | def free_space(
32 | self, path: str, tag: int | None = None
33 | ) -> Response[FreeSpaceResponse]:
34 | """Query for available free-space in the specified path."""
35 | return self._connection.send(
36 | Request(method="free-space", arguments={"path": path}, tag=tag),
37 | FreeSpaceResponse,
38 | )
39 |
40 | def bandwidth_groups(
41 | self, group: str | list[str] | None = None, tag: int | None = None
42 | ) -> Response[BandwidthGroupResponse]:
43 | """Query for bandwidth groups."""
44 | return self._connection.send(
45 | Request(method="group-get", arguments={"group": group}, tag=tag),
46 | BandwidthGroupResponse,
47 | )
48 |
--------------------------------------------------------------------------------
/clutch/method/queue.py:
--------------------------------------------------------------------------------
1 | from enum import Enum, unique
2 |
3 | from clutch.method.method import MethodNamespace
4 | from clutch.network.rpc.message import Request, Response
5 | from clutch.schema.user.method.shared import IdsArg
6 |
7 |
8 | @unique
9 | class QueueMovement(Enum):
10 | TOP = "queue-move-top"
11 | UP = "queue-move-up"
12 | DOWN = "queue-move-down"
13 | BOTTOM = "queue-move-bottom"
14 |
15 |
16 | class QueueMethods(MethodNamespace):
17 | def move(
18 | self, movement: QueueMovement, ids: IdsArg, tag: int | None = None
19 | ) -> Response:
20 | """Change the position of one or more torrents in the queue."""
21 | return self._connection.send(
22 | Request(method=movement.value, arguments={"ids": ids}, tag=tag)
23 | )
24 |
--------------------------------------------------------------------------------
/clutch/method/session.py:
--------------------------------------------------------------------------------
1 | from typing import Set
2 |
3 | from clutch.method.method import MethodNamespace
4 | from clutch.network.rpc.message import Request, Response
5 | from clutch.schema.request.session.accessor import SessionAccessorArgumentsRequest
6 | from clutch.schema.request.session.mutator import SessionMutatorArgumentsRequest
7 | from clutch.schema.user.method.session.accessor import SessionAccessorField
8 | from clutch.schema.user.method.session.mutator import SessionMutatorArguments
9 | from clutch.schema.user.response.session.accessor import SessionAccessor
10 | from clutch.schema.user.response.session.stats import SessionStats
11 |
12 |
13 | class SessionMethods(MethodNamespace):
14 | def accessor(
15 | self,
16 | fields: Set[SessionAccessorField] | None = None,
17 | tag: int | None = None,
18 | ) -> Response[SessionAccessor]:
19 | """Retrieve information about one or more torrents."""
20 | return self._connection.send(
21 | Request[SessionAccessorArgumentsRequest](
22 | method="session-get",
23 | arguments={"accessor_fields": fields},
24 | tag=tag,
25 | ),
26 | SessionAccessor,
27 | )
28 |
29 | def mutator(
30 | self, arguments: SessionMutatorArguments, tag: int | None = None
31 | ) -> Response:
32 | """Set a property of one or more torrents."""
33 | return self._connection.send(
34 | Request[SessionMutatorArgumentsRequest](
35 | method="session-set", arguments=arguments, tag=tag
36 | )
37 | )
38 |
39 | def stats(self, tag: int | None = None) -> Response[SessionStats]:
40 | """Retrieve all session statistics."""
41 | return self._connection.send(
42 | Request(method="session-stats", tag=tag), SessionStats
43 | )
44 |
45 | def shutdown(self, tag: int | None = None) -> Response:
46 | """Shutdown the torrent client."""
47 | return self._connection.send(Request(method="session-close", tag=tag))
48 |
--------------------------------------------------------------------------------
/clutch/method/shared.py:
--------------------------------------------------------------------------------
1 | from typing import Mapping
2 |
3 |
4 | def combine_arguments(
5 | arguments: Mapping[str, object] | None = None, **kwargs
6 | ) -> Mapping[str, object]:
7 | if arguments is None:
8 | arguments = {}
9 | else:
10 | arguments = dict(arguments)
11 | arguments.update({k: v for k, v in kwargs.items() if v is not None})
12 | return arguments
13 |
--------------------------------------------------------------------------------
/clutch/method/torrent.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Sequence, Set, Union
2 |
3 | from clutch.method.method import MethodNamespace
4 | from clutch.method.shared import combine_arguments
5 | from clutch.network.rpc.message import Request, Response
6 | from clutch.schema.request.torrent.accessor import TorrentAccessorArgumentsRequest
7 | from clutch.schema.request.torrent.add import TorrentAddArgumentsRequest
8 | from clutch.schema.request.torrent.mutator import TorrentMutatorArgumentsRequest
9 | from clutch.schema.user.method.shared import IdsArg
10 | from clutch.schema.user.method.torrent.accessor import TorrentAccessorField, field_keys
11 | from clutch.schema.user.method.torrent.action import TorrentActionMethod
12 | from clutch.schema.user.method.torrent.add import TorrentAddArguments
13 | from clutch.schema.user.method.torrent.mutator import TorrentMutatorArguments
14 | from clutch.schema.user.response.torrent.accessor import TorrentAccessorResponse
15 | from clutch.schema.user.response.torrent.add import TorrentAdd
16 | from clutch.schema.user.response.torrent.rename import TorrentRename
17 |
18 |
19 | class TorrentMethods(MethodNamespace):
20 | def accessor(
21 | self,
22 | fields: Set[TorrentAccessorField] | None = None,
23 | *,
24 | all_fields: bool = False,
25 | ids: IdsArg | None = None,
26 | response_format: Literal["objects", "table"] | None = None,
27 | tag: int | None = None,
28 | ) -> Response[TorrentAccessorResponse]:
29 | """Retrieve information about one or more torrents."""
30 | if response_format is None:
31 | response_format = "objects"
32 |
33 | if all_fields:
34 | fields = field_keys
35 | elif fields is None:
36 | fields = set()
37 |
38 | combined_arguments = combine_arguments(
39 | accessor_fields=fields, format=response_format, ids=ids
40 | )
41 | return self._connection.send(
42 | Request[TorrentAccessorArgumentsRequest](
43 | method="torrent-get", arguments=combined_arguments, tag=tag
44 | ),
45 | TorrentAccessorResponse,
46 | )
47 |
48 | def action(
49 | self,
50 | method: TorrentActionMethod,
51 | ids: IdsArg | None = None,
52 | tag: int | None = None,
53 | ) -> Response:
54 | """Start, stop, verify or reannounce a torrent."""
55 | return self._connection.send(
56 | Request(method=method.value, arguments={"ids": ids}, tag=tag)
57 | )
58 |
59 | def add(
60 | self, arguments: TorrentAddArguments, tag: int | None = None
61 | ) -> Response[TorrentAdd]:
62 | """Add a new torrent."""
63 | return self._connection.send(
64 | Request[TorrentAddArgumentsRequest](
65 | method="torrent-add", arguments=arguments, tag=tag
66 | ),
67 | TorrentAdd,
68 | )
69 |
70 | def mutator(
71 | self,
72 | ids: IdsArg | None = None,
73 | arguments: TorrentMutatorArguments | None = None,
74 | tag: int | None = None,
75 | ) -> Response:
76 | """Set a property of one or more torrents."""
77 | try:
78 | arguments["tracker_list"] = "\n\n".join(
79 | "\n".join(x) for x in arguments["tracker_list"]
80 | )
81 | except (KeyError, TypeError):
82 | pass
83 | return self._connection.send(
84 | Request[TorrentMutatorArgumentsRequest](
85 | method="torrent-set",
86 | arguments=combine_arguments(arguments, ids=ids),
87 | tag=tag,
88 | )
89 | )
90 |
91 | def move(
92 | self, ids: IdsArg, location: str, move: bool = False, tag: int | None = None
93 | ) -> Response:
94 | """Change the storage location of a torrent."""
95 | return self._connection.send(
96 | Request(
97 | method="torrent-set-location",
98 | arguments=combine_arguments(ids=ids, location=location, move=move),
99 | tag=tag,
100 | )
101 | )
102 |
103 | def remove(
104 | self, ids: IdsArg, delete_local_data: bool = False, tag: int | None = None
105 | ) -> Response:
106 | """Remove one or more torrents."""
107 | return self._connection.send(
108 | Request(
109 | method="torrent-remove",
110 | arguments={"ids": ids, "delete-local-data": delete_local_data},
111 | tag=tag,
112 | )
113 | )
114 |
115 | def rename(
116 | self,
117 | ids: Sequence[Union[str, int]],
118 | path: str,
119 | name: str,
120 | tag: int | None = None,
121 | ) -> Response[TorrentRename]:
122 | """Rename a file or directory in a torrent."""
123 | return self._connection.send(
124 | Request(
125 | method="torrent-rename-path",
126 | arguments=combine_arguments(ids=ids, path=path, name=name),
127 | tag=tag,
128 | ),
129 | TorrentRename,
130 | )
131 |
--------------------------------------------------------------------------------
/clutch/network/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/network/__init__.py
--------------------------------------------------------------------------------
/clutch/network/connection.py:
--------------------------------------------------------------------------------
1 | from typing import Type, TypeVar
2 |
3 | from pydantic import BaseModel
4 |
5 | from clutch.network.rpc.message import Request, Response
6 | from clutch.network.session import TransmissionSession
7 |
8 | T = TypeVar("T", bound=BaseModel)
9 |
10 |
11 | class Connection:
12 | def __init__(
13 | self, endpoint: str, session: TransmissionSession, debug: bool = False
14 | ):
15 | self.endpoint = endpoint
16 | self.session = session
17 | self.debug = debug
18 |
19 | def send(self, request: Request, model: Type[T] | None = None) -> Response[T]:
20 | data = request.model_dump_json(by_alias=True, exclude_none=True).encode("utf-8")
21 | response = self.session.post(self.endpoint, data=data)
22 | if model is not None:
23 | return Response[model].model_validate_json(response.text) # type: ignore
24 | else:
25 | return Response.model_validate_json(response.text)
26 |
--------------------------------------------------------------------------------
/clutch/network/rpc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/network/rpc/__init__.py
--------------------------------------------------------------------------------
/clutch/network/rpc/convert.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import Mapping, MutableMapping, Sequence, Union
3 |
4 |
5 | def to_underscore(key: str) -> str:
6 | camelcase_split = re.split(r"(?<=[a-z])(?=[A-Z])", key)
7 | hyphen_split = key.split("-")
8 | if len(camelcase_split) > 1:
9 | return "_".join([word.lower() for word in camelcase_split])
10 | elif len(hyphen_split) > 1:
11 | return "_".join([word.lower() for word in hyphen_split])
12 | else:
13 | return key
14 |
15 |
16 | def to_camel(key: str) -> str:
17 | words = key.split("_")
18 | return "".join(words[:1] + [word.capitalize() for word in words[1:]])
19 |
20 |
21 | def to_hyphen(key: str) -> str:
22 | return "-".join(key.split("_"))
23 |
24 |
25 | def normalize_arguments(
26 | arguments: Mapping[str, object] | None = None,
27 | ) -> Mapping[str, object]:
28 | if arguments is None:
29 | return {}
30 |
31 | result: MutableMapping[str, object] = {}
32 | iterations = [(result, item) for item in arguments.items()]
33 | for iteration in iterations:
34 | parent: Union[MutableMapping[str, object], Sequence] = iteration[0]
35 | if isinstance(parent, dict):
36 | (key, value) = iteration[1]
37 | converted_key = to_underscore(key)
38 | if isinstance(value, dict):
39 | parent[converted_key] = {}
40 | iterations.extend(
41 | [(parent[converted_key], item) for item in value.items()]
42 | )
43 | elif isinstance(value, list):
44 | parent[converted_key] = []
45 | iterations.extend([(parent[converted_key], item) for item in value])
46 | else:
47 | parent[converted_key] = value
48 | elif isinstance(parent, list):
49 | value = iteration[1]
50 | if isinstance(value, dict):
51 | new_element = {}
52 | parent.append(new_element)
53 | iterations.extend([(new_element, item) for item in value.items()])
54 | elif isinstance(value, list):
55 | new_element = []
56 | parent.append(new_element)
57 | iterations.extend([(new_element, item) for item in value])
58 | else:
59 | parent.append(value)
60 | return result
61 |
--------------------------------------------------------------------------------
/clutch/network/rpc/message.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated, Any, Generic, TypeVar
2 |
3 | from pydantic import BaseModel, FieldSerializationInfo, PlainSerializer, field_validator
4 |
5 | from clutch.network.rpc.convert import normalize_arguments
6 |
7 | T = TypeVar("T")
8 |
9 |
10 | def dict_not_none_ser(
11 | value: dict[str, Any], info: FieldSerializationInfo
12 | ) -> dict[str, Any]:
13 | if isinstance(value, dict) and info.exclude_none:
14 | return {k: v for k, v in value.items() if v is not None}
15 | else:
16 | return value
17 |
18 |
19 | class Request(BaseModel, Generic[T]):
20 | """RPC request container"""
21 |
22 | method: str
23 | arguments: Annotated[T | None, PlainSerializer(dict_not_none_ser)] = None
24 | tag: int | None = None
25 |
26 |
27 | class Response(BaseModel, Generic[T]):
28 | """RPC response container"""
29 |
30 | result: str
31 | arguments: T | None = None
32 | tag: int | None = None
33 |
34 | @field_validator("arguments", mode="before")
35 | @classmethod
36 | def fields_underscored(cls, v: Any):
37 | if isinstance(v, dict):
38 | return normalize_arguments(v)
39 | else:
40 | return v
41 |
--------------------------------------------------------------------------------
/clutch/network/session.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urlparse
2 |
3 | from requests import Session
4 | from requests.auth import HTTPBasicAuth
5 |
6 |
7 | class TransmissionAuth(HTTPBasicAuth):
8 | HEADER_NAME = "X-Transmission-Session-Id"
9 | HEADER_NAME_LOWER = HEADER_NAME.lower()
10 |
11 | def __init__(self, username=None, password=None):
12 | # setup any auth-related data here
13 | self.__csrf_tokens = {}
14 | super(TransmissionAuth, self).__init__(username, password)
15 |
16 | def handle_409(self, r, **kwargs):
17 | """handles CSRF token expiration and resends the request"""
18 |
19 | # cannot handle the request
20 | if self.HEADER_NAME not in r.headers:
21 | return
22 |
23 | url = r.url
24 | self.__csrf_tokens[url] = r.headers[self.HEADER_NAME]
25 |
26 | # copy the original headers
27 | new_headers = dict(
28 | (k, v)
29 | for k, v in r.request.headers.items()
30 | if k.lower()
31 | not in ("content-length", "content-type", self.HEADER_NAME_LOWER)
32 | )
33 |
34 | new_headers["Host"] = urlparse(r.request.url).netloc
35 | new_request = r.request.copy()
36 | new_request.headers.update(new_headers)
37 | new_request.headers[self.HEADER_NAME] = self.__csrf_tokens[url]
38 |
39 | r.close()
40 | _r = r.connection.send(new_request, **kwargs)
41 | _r.history.append(r)
42 | _r.request = new_request
43 | return _r
44 |
45 | def __call__(self, r):
46 | """adds the appropriate CSRF token to the headers"""
47 | # don't use authentication if username and password aren't defined
48 | if self.username is not None and self.password is not None:
49 | r = super(TransmissionAuth, self).__call__(r)
50 |
51 | token = self.__csrf_tokens.get(r.url)
52 | if token:
53 | r.headers[self.HEADER_NAME] = token
54 | r.register_hook("response", self.handle_409)
55 | return r
56 |
57 |
58 | class TransmissionSession(Session):
59 | """
60 | Handles Transmission CSRF Protection
61 | https://trac.transmissionbt.com/browser/trunk/extras/rpc-spec.txt#L48
62 | """
63 |
64 | def __init__(self, username, password):
65 | super(TransmissionSession, self).__init__()
66 | self.auth = TransmissionAuth(username, password)
67 |
--------------------------------------------------------------------------------
/clutch/network/utility.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urlsplit, urlunsplit
2 |
3 |
4 | def make_endpoint(
5 | address: str = "http://localhost:9091/transmission/rpc",
6 | scheme: str | None = None,
7 | host: str | None = None,
8 | port: int | None = None,
9 | path: str | None = None,
10 | query: str | None = None,
11 | ) -> str:
12 | # any explicit keyword arguments override the default address
13 | url_info = urlsplit(address)
14 | if scheme is None:
15 | scheme = url_info.scheme
16 | if host is None:
17 | host = url_info.hostname
18 | if port is None:
19 | port = url_info.port
20 | if path is None:
21 | path = url_info.path
22 | if query is None:
23 | query = url_info.query
24 | return urlunsplit((scheme, f"{host}:{port}", path, query, None))
25 |
--------------------------------------------------------------------------------
/clutch/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/py.typed
--------------------------------------------------------------------------------
/clutch/schema/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/request/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/request/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/request/misc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/request/misc/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/request/misc/bandwidth_group.py:
--------------------------------------------------------------------------------
1 |
2 | from pydantic import BaseModel
3 |
4 |
5 | class BandwidthGroupArgumentsRequest(BaseModel):
6 | group: str | list[str] | None = None
7 |
--------------------------------------------------------------------------------
/clutch/schema/request/misc/port_test.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Tuple
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | IpProtocol = Tuple[Literal["ipv4"] | Literal["ipv6"]]
6 |
7 |
8 | class PortTestArgumentsRequest(BaseModel):
9 | ip_protocol: IpProtocol | None = Field(None, serialization_alias="ipProtocol")
10 |
--------------------------------------------------------------------------------
/clutch/schema/request/session/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/request/session/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/request/session/accessor.py:
--------------------------------------------------------------------------------
1 | from typing import Literal
2 |
3 | from pydantic import BaseModel, Field, field_validator
4 |
5 | from clutch.network.rpc.convert import to_camel, to_hyphen
6 |
7 | SessionAccessorRequestField = Literal[
8 | "alt-speed-down",
9 | "alt-speed-enabled",
10 | "alt-speed-time-begin",
11 | "alt-speed-time-day",
12 | "alt-speed-time-enabled",
13 | "alt-speed-time-end",
14 | "alt-speed-up",
15 | "blocklist-enabled",
16 | "blocklist-size",
17 | "blocklist-url",
18 | "cache-size-mb",
19 | "config-dir",
20 | "default-trackers",
21 | "dht-enabled",
22 | "download-dir",
23 | "download-queue-enabled",
24 | "download-queue-size",
25 | "encryption",
26 | "idle-seeding-limit-enabled",
27 | "idle-seeding-limit",
28 | "incomplete-dir-enabled",
29 | "incomplete-dir",
30 | "lpd-enabled",
31 | "peer-limit-global",
32 | "peer-limit-per-torrent",
33 | "peer-port-random-on-start",
34 | "peer-port",
35 | "pex-enabled",
36 | "port-forwarding-enabled",
37 | "queue-stalled-enabled",
38 | "queue-stalled-minutes",
39 | "rename-partial-files",
40 | "reqq",
41 | "rpc-version-minimum",
42 | "rpc-version-semver",
43 | "rpc-version",
44 | "script-torrent-added-enabled",
45 | "script-torrent-added-filename",
46 | "script-torrent-done-enabled",
47 | "script-torrent-done-filename",
48 | "script-torrent-done-seeding-enabled",
49 | "script-torrent-done-seeding-filename",
50 | "seed-queue-enabled",
51 | "seed-queue-size",
52 | "seedRatioLimit",
53 | "seedRatioLimited",
54 | "session-id",
55 | "speed-limit-down-enabled",
56 | "speed-limit-down",
57 | "speed-limit-up-enabled",
58 | "speed-limit-up",
59 | "start-added-torrents",
60 | "trash-original-torrent-files",
61 | "units",
62 | "utp-enabled",
63 | "version",
64 | ]
65 |
66 |
67 | class SessionAccessorArgumentsRequest(BaseModel):
68 | accessor_fields: set[SessionAccessorRequestField] | None = Field(
69 | None, serialization_alias="fields"
70 | )
71 |
72 | @field_validator("accessor_fields", mode="before")
73 | @classmethod
74 | def accessor_fields_format(cls, v):
75 | if v is None:
76 | return v
77 | camel = ["seed_ratio_limit", "seed_ratio_limited"]
78 | result = set()
79 | try:
80 | for field in v:
81 | if field in camel:
82 | result.add(to_camel(field))
83 | else:
84 | result.add(to_hyphen(field))
85 | except TypeError:
86 | return v
87 | return result
88 |
--------------------------------------------------------------------------------
/clutch/schema/request/session/mutator.py:
--------------------------------------------------------------------------------
1 | from typing import Self
2 |
3 | from pydantic import BaseModel, Field, model_validator
4 |
5 | from clutch.schema.request.session.shared import UnitsRequest
6 |
7 |
8 | class SessionMutatorArgumentsRequest(BaseModel):
9 | alt_speed_down: int | None = Field(None, serialization_alias="alt-speed-down")
10 | alt_speed_enabled: bool | None = Field(
11 | None, serialization_alias="alt-speed-enabled"
12 | )
13 | alt_speed_time_begin: int | None = Field(
14 | None, serialization_alias="alt-speed-time-begin"
15 | )
16 | alt_speed_time_enabled: bool | None = Field(
17 | None, serialization_alias="alt-speed-time-enabled"
18 | )
19 | alt_speed_time_end: int | None = Field(
20 | None, serialization_alias="alt-speed-time-end"
21 | )
22 | alt_speed_time_day: int | None = Field(
23 | None, serialization_alias="alt-speed-time-day"
24 | )
25 | alt_speed_up: int | None = Field(None, serialization_alias="alt-speed-up")
26 | blocklist_url: str | None = Field(None, serialization_alias="blocklist-url")
27 | blocklist_enabled: bool | None = Field(
28 | None, serialization_alias="blocklist-enabled"
29 | )
30 | cache_size_mb: int | None = Field(None, serialization_alias="cache-size-mb")
31 | download_dir: str | None = Field(None, serialization_alias="download-dir")
32 | download_queue_size: int | None = Field(
33 | None, serialization_alias="download-queue-size"
34 | )
35 | download_queue_enabled: bool | None = Field(
36 | None, serialization_alias="download-queue-enabled"
37 | )
38 | dht_enabled: bool | None = Field(None, serialization_alias="dht-enabled")
39 | encryption: str | None = None
40 | idle_seeding_limit: int | None = Field(
41 | None, serialization_alias="idle-seeding-limit"
42 | )
43 | idle_seeding_limit_enabled: bool | None = Field(
44 | None, serialization_alias="idle-seeding-limit-enabled"
45 | )
46 | incomplete_dir: str | None = Field(None, serialization_alias="incomplete-dir")
47 | incomplete_dir_enabled: bool | None = Field(
48 | None, serialization_alias="incomplete-dir-enabled"
49 | )
50 | lpd_enabled: bool | None = Field(None, serialization_alias="lpd-enabled")
51 | peer_limit_global: int | None = Field(
52 | None, serialization_alias="peer-limit-global"
53 | )
54 | peer_limit_per_torrent: int | None = Field(
55 | None, serialization_alias="peer-limit-per-torrent"
56 | )
57 | pex_enabled: bool | None = Field(None, serialization_alias="pex-enabled")
58 | peer_port: int | None = Field(None, serialization_alias="peer-port")
59 | peer_port_random_on_start: bool | None = Field(
60 | None, serialization_alias="peer-port-random-on-start"
61 | )
62 | port_forwarding_enabled: bool | None = Field(
63 | None, serialization_alias="port-forwarding-enabled"
64 | )
65 | queue_stalled_enabled: bool | None = Field(
66 | None, serialization_alias="queue-stalled-enabled"
67 | )
68 | queue_stalled_minutes: int | None = Field(
69 | None, serialization_alias="queue-stalled-minutes"
70 | )
71 | rename_partial_files: bool | None = Field(
72 | None, serialization_alias="rename-partial-files"
73 | )
74 | script_torrent_done_filename: str | None = Field(
75 | None, serialization_alias="script-torrent-done-filename"
76 | )
77 | script_torrent_done_enabled: bool | None = Field(
78 | None, serialization_alias="script-torrent-done-enabled"
79 | )
80 | seed_ratio_limit: float | None = Field(
81 | None, serialization_alias="seedRatioLimit"
82 | )
83 | seed_ratio_limited: bool | None = Field(
84 | None, serialization_alias="seedRatioLimited"
85 | )
86 | seed_queue_size: int | None = Field(None, serialization_alias="seed-queue-size")
87 | seed_queue_enabled: bool | None = Field(
88 | None, serialization_alias="seed-queue-enabled"
89 | )
90 | speed_limit_down: int | None = Field(
91 | None, serialization_alias="speed-limit-down"
92 | )
93 | speed_limit_down_enabled: bool | None = Field(
94 | None, serialization_alias="speed-limit-down-enabled"
95 | )
96 | speed_limit_up: int | None = Field(None, serialization_alias="speed-limit-up")
97 | speed_limit_up_enabled: bool | None = Field(
98 | None, serialization_alias="speed-limit-up-enabled"
99 | )
100 | start_added_torrents: bool | None = Field(
101 | None, serialization_alias="start-added-torrents"
102 | )
103 | trash_original_torrent_files: bool | None = Field(
104 | None, serialization_alias="trash-original-torrent-files"
105 | )
106 | units: UnitsRequest | None = None
107 | utp_enabled: bool | None = Field(None, serialization_alias="utp-enabled")
108 |
109 | @model_validator(mode="after")
110 | def at_least_one_field(self) -> Self:
111 | dumped = self.model_dump()
112 | if len(dumped) < 1:
113 | raise ValueError("At least one valid argument must be supplied")
114 | return self
115 |
--------------------------------------------------------------------------------
/clutch/schema/request/session/shared.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Tuple
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | DataRateUnits = Tuple[
6 | Literal["KB/s"], Literal["MB/s"], Literal["GB/s"], Literal["TB/s"]
7 | ]
8 | DataSizeUnits = Tuple[Literal["KB"], Literal["MB"], Literal["GB"], Literal["TB"]]
9 | ByteDefinition = Literal[1000, 1024]
10 |
11 |
12 | class UnitsRequest(BaseModel):
13 | speed_units: DataRateUnits = Field(..., serialization_alias="speed-units")
14 | speed_bytes: ByteDefinition = Field(..., serialization_alias="speed-bytes")
15 | size_units: DataSizeUnits = Field(..., serialization_alias="size-units")
16 | size_bytes: ByteDefinition = Field(..., serialization_alias="size-bytes")
17 | memory_units: DataSizeUnits = Field(..., serialization_alias="memory-units")
18 | memory_bytes: ByteDefinition = Field(..., serialization_alias="memory-bytes")
19 |
--------------------------------------------------------------------------------
/clutch/schema/request/torrent/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/request/torrent/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/request/torrent/accessor.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Set
2 |
3 | from pydantic import BaseModel, Field, field_validator
4 |
5 | from clutch.network.rpc.convert import to_camel, to_hyphen
6 | from clutch.schema.user.method.shared import IdsArg
7 |
8 | AccessorFieldRequest = Literal[
9 | "activityDate",
10 | "addedDate",
11 | "availability",
12 | "bandwidthPriority",
13 | "comment",
14 | "corruptEver",
15 | "creator",
16 | "dateCreated",
17 | "desiredAvailable",
18 | "doneDate",
19 | "downloadDir",
20 | "downloadedEver",
21 | "downloadLimit",
22 | "downloadLimited",
23 | "editDate",
24 | "error",
25 | "errorString",
26 | "eta",
27 | "etaIdle",
28 | "file-count",
29 | "files",
30 | "fileStats",
31 | "group",
32 | "hashString",
33 | "haveUnchecked",
34 | "haveValid",
35 | "honorsSessionLimits",
36 | "id",
37 | "isFinished",
38 | "isPrivate",
39 | "isStalled",
40 | "labels",
41 | "leftUntilDone",
42 | "magnetLink",
43 | "manualAnnounceTime",
44 | "maxConnectedPeers",
45 | "metadataPercentComplete",
46 | "name",
47 | "peer-limit",
48 | "peers",
49 | "peersConnected",
50 | "peersFrom",
51 | "peersGettingFromUs",
52 | "peersSendingToUs",
53 | "percentComplete",
54 | "percentDone",
55 | "pieces",
56 | "pieceCount",
57 | "pieceSize",
58 | "priorities",
59 | "primary-mime-type",
60 | "queuePosition",
61 | "rateDownload",
62 | "rateUpload",
63 | "recheckProgress",
64 | "secondsDownloading",
65 | "secondsSeeding",
66 | "seedIdleLimit",
67 | "seedIdleMode",
68 | "seedRatioLimit",
69 | "seedRatioMode",
70 | "sequentialDownload",
71 | "sizeWhenDone",
72 | "startDate",
73 | "status",
74 | "trackers",
75 | "trackerList",
76 | "trackerStats",
77 | "totalSize",
78 | "torrentFile",
79 | "uploadedEver",
80 | "uploadLimit",
81 | "uploadLimited",
82 | "uploadRatio",
83 | "wanted",
84 | "webseeds",
85 | "webseedsSendingToUs",
86 | ]
87 |
88 | TorrentAccessorFieldsRequest = Set[AccessorFieldRequest]
89 |
90 |
91 | class TorrentAccessorArgumentsRequest(BaseModel):
92 | ids: IdsArg | None = None
93 | format: Literal["objects", "table"] | None = None
94 | accessor_fields: TorrentAccessorFieldsRequest | None = Field(
95 | ..., serialization_alias="fields"
96 | )
97 |
98 | @field_validator("accessor_fields", mode="before")
99 | @classmethod
100 | def accessor_fields_format(cls, v):
101 | if v is not None:
102 | hyphenated = {"peer_limit"}
103 | result = set()
104 | try:
105 | for field in v:
106 | if field in hyphenated:
107 | result.add(to_hyphen(field))
108 | else:
109 | result.add(to_camel(field))
110 | except TypeError:
111 | return v
112 | return result
113 | else:
114 | return v
115 |
--------------------------------------------------------------------------------
/clutch/schema/request/torrent/add.py:
--------------------------------------------------------------------------------
1 | from typing import Self, Sequence
2 |
3 | from pydantic import BaseModel, Field, model_validator
4 |
5 |
6 | class Cookie(BaseModel):
7 | name: str
8 | content: str
9 |
10 |
11 | class TorrentAddArgumentsRequest(BaseModel):
12 | cookies: Sequence[Cookie] | None = None
13 | download_dir: str | None = Field(None, serialization_alias="download-dir")
14 | filename: str | None = None
15 | metainfo: str | None = None
16 | paused: bool | None = None
17 | peer_limit: int | None = Field(None, serialization_alias="peer-limit")
18 | bandwidth_priority: int | None = Field(
19 | None, serialization_alias="bandwidthPriority"
20 | )
21 | files_wanted: Sequence[int] | None = Field(
22 | None, serialization_alias="files-wanted"
23 | )
24 | files_unwanted: Sequence[int] | None = Field(
25 | None, serialization_alias="files-unwanted"
26 | )
27 | priority_high: Sequence[int] | None = Field(
28 | None, serialization_alias="priority-high"
29 | )
30 | priority_low: Sequence[int] | None = Field(
31 | None, serialization_alias="priority-low"
32 | )
33 | priority_normal: Sequence[int] | None = Field(
34 | None, serialization_alias="priority-normal"
35 | )
36 |
37 | @model_validator(mode="after")
38 | def check_required_exclusive_fields(self) -> Self:
39 | if self.filename is not None and self.metainfo is not None:
40 | raise ValueError("Both filename and metainfo fields are in request")
41 | if self.filename is None and self.metainfo is None:
42 | raise ValueError("Either filename or metainfo field is required in request")
43 | return self
44 |
--------------------------------------------------------------------------------
/clutch/schema/request/torrent/mutator.py:
--------------------------------------------------------------------------------
1 | from typing import Sequence
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | from clutch.schema.user.method.shared import IdsArg
6 |
7 |
8 | class TorrentMutatorArgumentsRequest(BaseModel):
9 | bandwidth_priority: int | None = Field(
10 | None, serialization_alias="bandwidthPriority"
11 | )
12 | download_limit: int | None = Field(None, serialization_alias="downloadLimit")
13 | download_limited: bool | None = Field(None, serialization_alias="downloadLimited")
14 | edit_date: int | None = Field(None, serialization_alias="editDate")
15 | files_wanted: Sequence[int] | None = Field(None, serialization_alias="files-wanted")
16 | files_unwanted: Sequence[int] | None = Field(
17 | None, serialization_alias="files-unwanted"
18 | )
19 | group: str | None = Field(None)
20 | honors_session_limits: bool | None = Field(
21 | None, serialization_alias="honorsSessionLimits"
22 | )
23 | ids: IdsArg | None = None
24 | labels: Sequence[str] | None = None
25 | location: str | None = None
26 | peer_limit: int | None = Field(None, serialization_alias="peer-limit")
27 | priority_high: Sequence[int] | None = Field(
28 | None, serialization_alias="priority-high"
29 | )
30 | priority_low: Sequence[int] | None = Field(None, serialization_alias="priority-low")
31 | priority_normal: Sequence[int] | None = Field(
32 | None, serialization_alias="priority-normal"
33 | )
34 | queue_position: int | None = Field(None, serialization_alias="queuePosition")
35 | seed_idle_limit: int | None = Field(None, serialization_alias="seedIdleLimit")
36 | seed_idle_mode: int | None = Field(None, serialization_alias="seedIdleMode")
37 | seed_ratio_limit: float | None = Field(None, serialization_alias="seedRatioLimit")
38 | seed_ratio_mode: int | None = Field(None, serialization_alias="seedRatioMode")
39 | tracker_list: str | None = Field(None, serialization_alias="trackerList")
40 | upload_limit: int | None = Field(None, serialization_alias="uploadLimit")
41 | upload_limited: bool | None = Field(None, serialization_alias="uploadLimited")
42 |
--------------------------------------------------------------------------------
/clutch/schema/user/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/user/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/user/method/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/user/method/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/user/method/misc.py:
--------------------------------------------------------------------------------
1 | from enum import Enum, unique
2 |
3 |
4 | @unique
5 | class IpProtocol(Enum):
6 | IPV6 = "ipv6"
7 | IPV4 = "ipv4"
8 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/session/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/user/method/session/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/user/method/session/accessor.py:
--------------------------------------------------------------------------------
1 | from typing import Literal
2 |
3 | SessionAccessorField = Literal[
4 | "alt_speed_down",
5 | "alt_speed_enabled",
6 | "alt_speed_time_begin",
7 | "alt_speed_time_day",
8 | "alt_speed_time_enabled",
9 | "alt_speed_time_end",
10 | "alt_speed_up",
11 | "blocklist_enabled",
12 | "blocklist_size",
13 | "blocklist_url",
14 | "cache_size_mb",
15 | "config_dir",
16 | "default_trackers",
17 | "dht_enabled",
18 | "download_dir",
19 | "download_queue_enabled",
20 | "download_queue_size",
21 | "encryption",
22 | "idle_seeding_limit_enabled",
23 | "idle_seeding_limit",
24 | "incomplete_dir_enabled",
25 | "incomplete_dir",
26 | "lpd_enabled",
27 | "peer_limit_global",
28 | "peer_limit_per_torrent",
29 | "peer_port_random_on_start",
30 | "peer_port",
31 | "pex_enabled",
32 | "port_forwarding_enabled",
33 | "queue_stalled_enabled",
34 | "queue_stalled_minutes",
35 | "rename_partial_files",
36 | "reqq",
37 | "rpc_version_minimum",
38 | "rpc_version_semver",
39 | "rpc_version",
40 | "script_torrent_added_enabled",
41 | "script_torrent_added_filename",
42 | "script_torrent_done_enabled",
43 | "script_torrent_done_filename",
44 | "script_torrent_done_seeding_enabled",
45 | "script_torrent_done_seeding_filename",
46 | "seed_queue_enabled",
47 | "seed_queue_size",
48 | "seed_ratio_limit",
49 | "seed_ratio_limited",
50 | "session_id",
51 | "speed_limit_down_enabled",
52 | "speed_limit_down",
53 | "speed_limit_up_enabled",
54 | "speed_limit_up",
55 | "start_added_torrents",
56 | "trash_original_torrent_files",
57 | "units",
58 | "utp_enabled",
59 | "version",
60 | ]
61 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/session/mutator.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict
2 |
3 | from clutch.schema.user.method.session.shared import Units
4 |
5 |
6 | class SessionMutatorArguments(TypedDict, total=False):
7 | alt_speed_down: int
8 | alt_speed_enabled: bool
9 | alt_speed_time_begin: int
10 | alt_speed_time_enabled: bool
11 | alt_speed_time_end: int
12 | alt_speed_time_day: int
13 | alt_speed_up: int
14 | blocklist_url: str
15 | blocklist_enabled: bool
16 | cache_size_mb: int
17 | download_dir: str
18 | download_queue_size: int
19 | download_queue_enabled: bool
20 | dht_enabled: bool
21 | encryption: str
22 | idle_seeding_limit: int
23 | idle_seeding_limit_enabled: bool
24 | incomplete_dir: str
25 | incomplete_dir_enabled: bool
26 | lpd_enabled: bool
27 | peer_limit_global: int
28 | peer_limit_per_torrent: int
29 | pex_enabled: bool
30 | peer_port: int
31 | peer_port_random_on_start: bool
32 | port_forwarding_enabled: bool
33 | queue_stalled_enabled: bool
34 | queue_stalled_minutes: int
35 | rename_partial_files: bool
36 | script_torrent_done_filename: str
37 | script_torrent_done_enabled: bool
38 | seed_ratio_limit: float
39 | seed_ratio_limited: bool
40 | seed_queue_size: int
41 | seed_queue_enabled: bool
42 | speed_limit_down: int
43 | speed_limit_down_enabled: bool
44 | speed_limit_up: int
45 | speed_limit_up_enabled: bool
46 | start_added_torrents: bool
47 | trash_original_torrent_files: bool
48 | units: Units
49 | utp_enabled: bool
50 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/session/shared.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Tuple, TypedDict
2 |
3 | # Todo: change these values, match to response schema
4 | DataRateUnits = Tuple[
5 | Literal["kB/s"], Literal["MB/s"], Literal["GB/s"], Literal["TB/s"]
6 | ]
7 | DataSizeUnits = Tuple[Literal["kB"], Literal["MB"], Literal["GB"], Literal["TB"]]
8 | ByteDefinition = Literal[1000, 1024]
9 |
10 |
11 | class Units(TypedDict):
12 | speed_units: DataRateUnits
13 | speed_bytes: ByteDefinition
14 | size_units: DataSizeUnits
15 | size_bytes: ByteDefinition
16 | memory_units: DataSizeUnits
17 | memory_bytes: ByteDefinition
18 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/shared.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Union, Iterable
2 |
3 | TorrentId = Union[int, str]
4 |
5 | IdsArg = Union[
6 | int, Iterable[TorrentId], Iterable[int], Iterable[str], Literal["recently_active"]
7 | ]
8 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/torrent/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/user/method/torrent/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/user/method/torrent/accessor.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Set, TypedDict
2 |
3 | from clutch.schema.user.method.shared import IdsArg
4 |
5 | TorrentAccessorField = Literal[
6 | "activity_date",
7 | "added_date",
8 | "availability",
9 | "bandwidth_priority",
10 | "comment",
11 | "corrupt_ever",
12 | "creator",
13 | "date_created",
14 | "desired_available",
15 | "done_date",
16 | "download_dir",
17 | "downloaded_ever",
18 | "download_limit",
19 | "download_limited",
20 | "edit_date",
21 | "error",
22 | "error_string",
23 | "eta",
24 | "eta_idle",
25 | "file_count",
26 | "files",
27 | "file_stats",
28 | "group",
29 | "hash_string",
30 | "have_unchecked",
31 | "have_valid",
32 | "honors_session_limits",
33 | "id",
34 | "is_finished",
35 | "is_private",
36 | "is_stalled",
37 | "labels",
38 | "left_until_done",
39 | "magnet_link",
40 | "manual_announce_time",
41 | "max_connected_peers",
42 | "metadata_percent_complete",
43 | "name",
44 | "peer_limit",
45 | "peers",
46 | "peers_connected",
47 | "peers_from",
48 | "peers_getting_from_us",
49 | "peers_sending_to_us",
50 | "percent_complete",
51 | "percent_done",
52 | "pieces",
53 | "piece_count",
54 | "piece_size",
55 | "priorities",
56 | "primary_mime_type",
57 | "queue_position",
58 | "rate_download",
59 | "rate_upload",
60 | "recheck_progress",
61 | "seconds_downloading",
62 | "seconds_seeding",
63 | "seed_idle_limit",
64 | "seed_idle_mode",
65 | "seed_ratio_limit",
66 | "seed_ratio_mode",
67 | "sequential_download",
68 | "size_when_done",
69 | "start_date",
70 | "status",
71 | "trackers",
72 | "tracker_list",
73 | "tracker_stats",
74 | "total_size",
75 | "torrent_file",
76 | "uploaded_ever",
77 | "upload_limit",
78 | "upload_limited",
79 | "upload_ratio",
80 | "wanted",
81 | "webseeds",
82 | "webseeds_sending_to_us",
83 | ]
84 |
85 | TorrentAccessorFields = Set[TorrentAccessorField]
86 |
87 |
88 | class TorrentAccessorArgumentsOptional(TypedDict, total=False):
89 | ids: IdsArg
90 | format: Literal["objects", "table"]
91 |
92 |
93 | class TorrentAccessorArguments(TorrentAccessorArgumentsOptional):
94 | fields: TorrentAccessorFields
95 |
96 |
97 | field_keys: set[TorrentAccessorField] = {
98 | "activity_date",
99 | "added_date",
100 | "availability",
101 | "bandwidth_priority",
102 | "comment",
103 | "corrupt_ever",
104 | "creator",
105 | "date_created",
106 | "desired_available",
107 | "done_date",
108 | "download_dir",
109 | "downloaded_ever",
110 | "download_limit",
111 | "download_limited",
112 | "edit_date",
113 | "error",
114 | "error_string",
115 | "eta",
116 | "eta_idle",
117 | "file_count",
118 | "files",
119 | "file_stats",
120 | "group",
121 | "hash_string",
122 | "have_unchecked",
123 | "have_valid",
124 | "honors_session_limits",
125 | "id",
126 | "is_finished",
127 | "is_private",
128 | "is_stalled",
129 | "labels",
130 | "left_until_done",
131 | "magnet_link",
132 | "manual_announce_time",
133 | "max_connected_peers",
134 | "metadata_percent_complete",
135 | "name",
136 | "peer_limit",
137 | "peers",
138 | "peers_connected",
139 | "peers_from",
140 | "peers_getting_from_us",
141 | "peers_sending_to_us",
142 | "percent_complete",
143 | "percent_done",
144 | "pieces",
145 | "piece_count",
146 | "piece_size",
147 | "priorities",
148 | "primary_mime_type",
149 | "queue_position",
150 | "rate_download",
151 | "rate_upload",
152 | "recheck_progress",
153 | "seconds_downloading",
154 | "seconds_seeding",
155 | "seed_idle_limit",
156 | "seed_idle_mode",
157 | "seed_ratio_limit",
158 | "seed_ratio_mode",
159 | "sequential_download",
160 | "size_when_done",
161 | "start_date",
162 | "status",
163 | "trackers",
164 | "tracker_list",
165 | "tracker_stats",
166 | "total_size",
167 | "torrent_file",
168 | "uploaded_ever",
169 | "upload_limit",
170 | "upload_limited",
171 | "upload_ratio",
172 | "wanted",
173 | "webseeds",
174 | "webseeds_sending_to_us",
175 | }
176 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/torrent/action.py:
--------------------------------------------------------------------------------
1 | from enum import Enum, unique
2 |
3 |
4 | @unique
5 | class TorrentActionMethod(Enum):
6 | START = "torrent-start"
7 | START_NOW = "torrent-start-now"
8 | STOP = "torrent-stop"
9 | VERIFY = "torrent-verify"
10 | REANNOUNCE = "torrent-reannounce"
11 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/torrent/add.py:
--------------------------------------------------------------------------------
1 | from typing import Sequence, TypedDict, Union
2 |
3 |
4 | class Cookie(TypedDict):
5 | name: str
6 | content: str
7 |
8 |
9 | class TorrentAddArgumentsOptional(TypedDict, total=False):
10 | cookies: Sequence[Cookie]
11 | download_dir: str
12 | paused: bool
13 | peer_limit: int
14 | bandwidth_priority: int
15 | files_wanted: Sequence[int]
16 | files_unwanted: Sequence[int]
17 | priority_high: Sequence[int]
18 | priority_low: Sequence[int]
19 | priority_normal: Sequence[int]
20 |
21 |
22 | class TorrentAddByFilenameArguments(TorrentAddArgumentsOptional):
23 | filename: str
24 |
25 |
26 | class TorrentAddByMetainfoArguments(TorrentAddArgumentsOptional):
27 | metainfo: str
28 |
29 |
30 | TorrentAddArguments = Union[
31 | TorrentAddByFilenameArguments, TorrentAddByMetainfoArguments
32 | ]
33 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/torrent/move.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict
2 |
3 | from clutch.schema.user.method.shared import IdsArg
4 |
5 |
6 | class TorrentMoveArgumentsOptional(TypedDict, total=False):
7 | move: bool
8 |
9 |
10 | class TorrentMoveArguments(TorrentMoveArgumentsOptional):
11 | ids: IdsArg
12 | location: str
13 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/torrent/mutator.py:
--------------------------------------------------------------------------------
1 | from typing import NewType, Sequence, TypedDict
2 |
3 | from clutch.schema.user.method.shared import IdsArg
4 |
5 | Url = NewType("Url", str)
6 |
7 |
8 | class TorrentMutatorArguments(TypedDict, total=False):
9 | bandwidth_priority: int
10 | download_limit: int
11 | download_limited: bool
12 | files_wanted: Sequence[int] # empty is shorthand for all
13 | files_unwanted: Sequence[int] # empty is shorthand for all
14 | group: str
15 | honors_session_limits: bool
16 | ids: IdsArg
17 | labels: Sequence[str]
18 | location: str
19 | peer_limit: int
20 | priority_high: Sequence[int] # empty is shorthand for all
21 | priority_low: Sequence[int] # empty is shorthand for all
22 | priority_normal: Sequence[int] # empty is shorthand for all
23 | queue_position: int
24 | seed_idle_limit: int
25 | seed_idle_mode: int
26 | seed_ratio_limit: float
27 | seed_ratio_mode: int
28 | sequential_download: bool
29 | tracker_list: Sequence[Sequence[Url]]
30 | upload_limit: int
31 | upload_limited: bool
32 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/torrent/remove.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict
2 |
3 | from clutch.schema.user.method.shared import IdsArg
4 |
5 |
6 | class TorrentRemoveArgumentsOptional(TypedDict, total=False):
7 | delete_local_data: bool
8 |
9 |
10 | class TorrentRemoveArguments(TorrentRemoveArgumentsOptional):
11 | ids: IdsArg
12 |
--------------------------------------------------------------------------------
/clutch/schema/user/method/torrent/rename.py:
--------------------------------------------------------------------------------
1 | from typing import Sequence, TypedDict, Union
2 |
3 |
4 | class TorrentRenameArguments(TypedDict):
5 | ids: Sequence[Union[str, int]]
6 | path: str
7 | name: str
8 |
--------------------------------------------------------------------------------
/clutch/schema/user/response/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/user/response/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/user/response/misc.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | from clutch.schema.user.method.misc import IpProtocol
4 |
5 |
6 | class BlocklistResponse(BaseModel):
7 | blocklist_size: int
8 |
9 |
10 | class PortTestResponse(BaseModel):
11 | port_is_open: int
12 | ip_protocol: IpProtocol | None = None
13 |
14 |
15 | class FreeSpaceResponse(BaseModel):
16 | path: str
17 | size_bytes: int
18 | total_size: int
19 |
20 |
21 | class BandwidthGroup(BaseModel):
22 | honors_session_limits: bool
23 | name: str
24 | speed_limit_down_enabled: bool
25 | speed_limit_down: int
26 | speed_limit_up_enabled: bool
27 | speed_limit_up: int
28 |
29 |
30 | class BandwidthGroupResponse(BaseModel):
31 | group: list[BandwidthGroup]
32 |
--------------------------------------------------------------------------------
/clutch/schema/user/response/session/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/user/response/session/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/user/response/session/accessor.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated, Any, Literal, Tuple, Union
2 |
3 | from pydantic import AfterValidator, BaseModel
4 |
5 | BinaryDataRateUnits = Tuple[
6 | Literal["KiB/s"], Literal["MiB/s"], Literal["GiB/s"], Literal["TiB/s"]
7 | ]
8 | DecimalDataRateUnits = Tuple[
9 | Literal["kB/s", "KB/s"], Literal["MB/s"], Literal["GB/s"], Literal["TB/s"]
10 | ]
11 | DataRateUnits = Union[BinaryDataRateUnits, DecimalDataRateUnits]
12 |
13 | BinaryDataSizeUnits = Tuple[
14 | Literal["KiB"], Literal["MiB"], Literal["GiB"], Literal["TiB"]
15 | ]
16 | DecimalDataSizeUnits = Tuple[
17 | Literal["kB", "KB"], Literal["MB"], Literal["GB"], Literal["TB"]
18 | ]
19 | DataSizeUnits = Union[BinaryDataSizeUnits, DecimalDataSizeUnits]
20 |
21 | ByteDefinition = Literal[1000, 1024]
22 |
23 |
24 | class Units(BaseModel):
25 | speed_units: DataRateUnits
26 | speed_bytes: ByteDefinition
27 | size_units: DataSizeUnits
28 | size_bytes: ByteDefinition
29 | memory_units: DataSizeUnits
30 | memory_bytes: ByteDefinition
31 |
32 |
33 | def validate_tiers(v: Any) -> list[list[str]]:
34 | if not isinstance(v, str):
35 | raise ValueError(f"value {v} is not a string")
36 | return [x.split() for x in v.split("\n\n")]
37 |
38 |
39 | class SessionAccessor(BaseModel):
40 | alt_speed_down: int | None
41 | alt_speed_enabled: bool | None
42 | alt_speed_time_begin: int | None
43 | alt_speed_time_enabled: bool | None
44 | alt_speed_time_end: int | None
45 | alt_speed_time_day: int | None
46 | alt_speed_up: int | None
47 | blocklist_url: str | None
48 | blocklist_size: int | None
49 | blocklist_enabled: bool | None
50 | cache_size_mb: int | None
51 | config_dir: str | None
52 | default_trackers: Annotated[list[list[str]] | None, AfterValidator(validate_tiers)]
53 | download_dir: str | None
54 | download_queue_size: int | None
55 | download_queue_enabled: bool | None
56 | dht_enabled: bool | None
57 | encryption: str | None
58 | idle_seeding_limit: int | None
59 | idle_seeding_limit_enabled: bool | None
60 | incomplete_dir: str | None
61 | incomplete_dir_enabled: bool | None
62 | lpd_enabled: bool | None
63 | peer_limit_global: int | None
64 | peer_limit_per_torrent: int | None
65 | pex_enabled: bool | None
66 | peer_port: int | None
67 | peer_port_random_on_start: bool | None
68 | port_forwarding_enabled: bool | None
69 | queue_stalled_enabled: bool | None
70 | queue_stalled_minutes: int | None
71 | rename_partial_files: bool | None
72 | reqq: int | None
73 | rpc_version: int | None
74 | rpc_version_minimum: int | None
75 | rpc_version_semver: str | None
76 | script_torrent_added_enabled: bool | None
77 | script_torrent_added_filename: str | None
78 | script_torrent_done_seeding_enabled: bool | None
79 | script_torrent_done_seeding_filename: str | None
80 | script_torrent_done_filename: str | None
81 | script_torrent_done_enabled: bool | None
82 | seed_ratio_limit: float | None
83 | seed_ratio_limited: bool | None
84 | seed_queue_size: int | None
85 | seed_queue_enabled: bool | None
86 | session_id: str | None
87 | speed_limit_down: int | None
88 | speed_limit_down_enabled: bool | None
89 | speed_limit_up: int | None
90 | speed_limit_up_enabled: bool | None
91 | start_added_torrents: bool | None
92 | trash_original_torrent_files: bool | None
93 | units: Units | None
94 | utp_enabled: bool | None
95 | version: str | None
96 |
--------------------------------------------------------------------------------
/clutch/schema/user/response/session/stats.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class Stats(BaseModel):
5 | uploaded_bytes: int
6 | downloaded_bytes: int
7 | files_added: int
8 | session_count: int
9 | seconds_active: int
10 |
11 |
12 | class SessionStats(BaseModel):
13 | active_torrent_count: int
14 | download_speed: int
15 | paused_torrent_count: int
16 | torrent_count: int
17 | upload_speed: int
18 | cumulative_stats: Stats
19 | current_stats: Stats
20 |
--------------------------------------------------------------------------------
/clutch/schema/user/response/torrent/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/clutch/schema/user/response/torrent/__init__.py
--------------------------------------------------------------------------------
/clutch/schema/user/response/torrent/accessor.py:
--------------------------------------------------------------------------------
1 | from enum import Enum, unique
2 | from typing import Sequence, Union
3 |
4 | from pydantic import BaseModel
5 |
6 | from clutch.schema.user.method.torrent.accessor import TorrentAccessorField
7 |
8 |
9 | @unique
10 | class Status(Enum):
11 | STOPPED = 0 # Torrent is stopped
12 | CHECK_WAIT = 1 # Torrent is queued to verify local data
13 | CHECK = 2 # Torrent is verifying local data
14 | DOWNLOAD_WAIT = 3 # Torrent is queued to download
15 | DOWNLOAD = 4 # Torrent is downloading
16 | SEED_WAIT = 5 # Torrent is queued to seed
17 | SEED = 6 # Torrent is seeding
18 |
19 |
20 | class File(BaseModel):
21 | bytes_completed: int
22 | length: int
23 | name: str
24 | begin_piece: int
25 | end_piece: int
26 |
27 |
28 | class FileStats(BaseModel):
29 | bytes_completed: int
30 | wanted: bool
31 | priority: int
32 |
33 |
34 | class Peer(BaseModel):
35 | address: str
36 | client_name: str
37 | client_is_choked: bool
38 | client_is_interested: bool
39 | flag_str: str
40 | is_downloading_from: bool
41 | is_encrypted: bool
42 | is_incoming: bool
43 | is_uploading_to: bool
44 | is_utp: bool
45 | peer_is_choked: bool
46 | peer_is_interested: bool
47 | port: int
48 | progress: float
49 | rate_to_client: int # B/s
50 | rate_to_peer: int # B/s
51 |
52 |
53 | class PeersFrom(BaseModel):
54 | from_cache: int
55 | from_dht: int
56 | from_incoming: int
57 | from_lpd: int
58 | from_ltep: int
59 | from_pex: int
60 | from_tracker: int
61 |
62 |
63 | class Tracker(BaseModel):
64 | announce: str
65 | id: int
66 | scrape: str
67 | sitename: str
68 | tier: int
69 |
70 |
71 | class TrackerStat(BaseModel):
72 | announce: str
73 | announce_state: int
74 | download_count: int
75 | has_announced: bool
76 | has_scraped: bool
77 | host: str
78 | id: int
79 | is_backup: bool
80 | last_announce_peer_count: int
81 | last_announce_result: str
82 | last_announce_start_time: int
83 | last_announce_succeeded: bool
84 | last_announce_time: int
85 | last_announce_timed_out: bool
86 | last_scrape_result: str
87 | last_scrape_start_time: int
88 | last_scrape_succeeded: bool
89 | last_scrape_time: int
90 | last_scrape_timed_out: bool
91 | leecher_count: int
92 | next_announce_time: int
93 | next_scrape_time: int
94 | scrape: str
95 | scrape_state: int
96 | seeder_count: int
97 | sitename: str
98 | tier: int
99 |
100 |
101 | class TorrentAccessorObject(BaseModel):
102 | activity_date: int | None = None
103 | added_date: int | None = None
104 | availability: list[int] | None = (
105 | None # An array of `pieceCount` numbers representing the number of
106 | )
107 | # connected peers that have each piece, or -1 if we already have the piece ourselves.
108 | bandwidth_priority: int | None = None
109 | comment: str | None = None
110 | corrupt_ever: int | None = None
111 | creator: str | None = None
112 | date_created: int | None = None
113 | desired_available: int | None = None
114 | done_date: int | None = None
115 | download_dir: str | None = None
116 | downloaded_ever: int | None = None
117 | download_limit: int | None = None
118 | download_limited: bool | None = None
119 | edit_date: int | None = None
120 | error: int | None = None
121 | error_string: str | None = None
122 | eta: int | None = None
123 | eta_idle: int | None = None
124 | file_count: int | None = None
125 | files: Sequence[File] | None = None
126 | file_stats: Sequence[FileStats] | None = None
127 | group: str | None = None
128 | hash_string: str | None = None
129 | have_unchecked: int | None = None
130 | have_valid: int | None = None
131 | honors_session_limits: bool | None = None
132 | id: int | None = None
133 | is_finished: bool | None = None
134 | is_private: bool | None = None
135 | is_stalled: bool | None = None
136 | labels: Sequence[str] | None = None
137 | left_until_done: int | None = None
138 | magnet_link: str | None = None
139 | manual_announce_time: int | None = None
140 | max_connected_peers: int | None = None
141 | metadata_percent_complete: float | None = None
142 | name: str | None = None
143 | peer_limit: int | None = None
144 | peers: Sequence[Peer] | None = None
145 | peers_connected: int | None = None
146 | peers_from: PeersFrom | None = None
147 | peers_getting_from_us: int | None = None
148 | peers_sending_to_us: int | None = None
149 | percent_complete: float | None = None
150 | percent_done: float | None = None
151 | pieces: str | None = None
152 | piece_count: int | None = None
153 | piece_size: int | None = None
154 | priorities: Sequence[int] | None = None
155 | primary_mime_type: str | None = None
156 | queue_position: int | None = None
157 | rate_download: int | None = None
158 | rate_upload: int | None = None
159 | recheck_progress: float | None = None
160 | seconds_downloading: int | None = None
161 | seconds_seeding: int | None = None
162 | seed_idle_limit: int | None = None
163 | seed_idle_mode: int | None = None
164 | seed_ratio_limit: float | None = None
165 | seed_ratio_mode: int | None = None
166 | sequential_download: bool | None = None
167 | size_when_done: int | None = None
168 | start_date: int | None = None
169 | status: Status | None = None
170 | trackers: Sequence[Tracker] | None = None
171 | tracker_list: str | None = (
172 | None # string of announce URLs, one per line, with a blank line between tiers
173 | )
174 | tracker_stats: Sequence[TrackerStat] | None = None
175 | total_size: int | None = None
176 | torrent_file: str | None = None
177 | uploaded_ever: int | None = None
178 | upload_limit: int | None = None
179 | upload_limited: bool | None = None
180 | upload_ratio: float | None = None
181 | wanted: Sequence[bool] | None = (
182 | None # An array of `tr_torrentFileCount()` 0/1, 1 (true) if the corresponding file is to be downloaded.
183 | )
184 | webseeds: Sequence[str] | None = None
185 | webseeds_sending_to_us: int | None = None
186 |
187 |
188 | TorrentAccessorHeader = Sequence[TorrentAccessorField]
189 |
190 |
191 | TorrentAccessorTable = Union[Sequence, TorrentAccessorHeader]
192 |
193 |
194 | class TorrentAccessorResponse(BaseModel):
195 | removed: Sequence[int] | None = None
196 | torrents: (
197 | Union[Sequence[TorrentAccessorObject], Sequence[TorrentAccessorTable]] | None
198 | ) = None
199 |
--------------------------------------------------------------------------------
/clutch/schema/user/response/torrent/add.py:
--------------------------------------------------------------------------------
1 | from typing import Self
2 |
3 | from pydantic import BaseModel, model_validator
4 |
5 |
6 | class Torrent(BaseModel):
7 | id: int
8 | name: str
9 | hash_string: str
10 |
11 |
12 | class TorrentAdd(BaseModel):
13 | torrent_added: Torrent | None = None
14 | torrent_duplicate: Torrent | None = None
15 |
16 | @model_validator(mode="after")
17 | def check_exclusive_fields(self) -> Self:
18 | if self.torrent_added is not None and self.torrent_duplicate is not None:
19 | raise ValueError("Both torrent added and duplicate fields in response")
20 | return self
21 |
--------------------------------------------------------------------------------
/clutch/schema/user/response/torrent/rename.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class TorrentRename(BaseModel):
5 | path: str
6 | name: str
7 | id: int
8 |
--------------------------------------------------------------------------------
/docker/clutch.df:
--------------------------------------------------------------------------------
1 | FROM python:3.8-alpine
2 |
3 | ENV YOUR_ENV=${YOUR_ENV} \
4 | PYTHONFAULTHANDLER=1 \
5 | PYTHONUNBUFFERED=1 \
6 | PYTHONHASHSEED=random \
7 | PIP_NO_CACHE_DIR=off \
8 | PIP_DISABLE_PIP_VERSION_CHECK=on \
9 | PIP_DEFAULT_TIMEOUT=100 \
10 | POETRY_VERSION=1.0.2
11 |
12 | # Seemingly needed to compile poetry -- need to cut this down
13 | RUN apk update && apk add gcc libc-dev make git libffi-dev openssl-dev python3-dev libxml2-dev libxslt-dev
14 |
15 | # System deps:
16 | RUN pip install "poetry==$POETRY_VERSION"
17 |
18 | # Copy only requirements to cache them in docker layer
19 | WORKDIR /code
20 | COPY poetry.lock pyproject.toml /code/
21 | COPY ./docker/integration_resources/client_setup.py /code/
22 |
23 | # Project initialization:
24 | RUN poetry config virtualenvs.create false \
25 | && poetry install $(test "$YOUR_ENV" == production && echo "--no-dev") --no-interaction --no-ansi --no-root
26 |
27 | # Creating folders, and files for a project:
28 | COPY . /code
29 |
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | transmission:
4 | build:
5 | context: ..
6 | dockerfile: docker/transmission.df
7 | testbed:
8 | build:
9 | context: ..
10 | dockerfile: docker/clutch.df
11 | depends_on:
12 | - transmission
13 | start_dependencies:
14 | build:
15 | context: .
16 | dockerfile: integration-wait.df
17 | depends_on:
18 | - transmission
19 | command: transmission:9091
20 |
--------------------------------------------------------------------------------
/docker/integration-test-wait.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | : "${SLEEP_LENGTH:=2}"
4 | : "${TIMEOUT_LENGTH:=300}"
5 |
6 | wait_for() {
7 | START=$(date +%s)
8 | echo "Waiting for $1 to listen on $2..."
9 | while ! nc -z "$1" "$2";
10 | do
11 | if [ $(($(date +%s) - START)) -gt $TIMEOUT_LENGTH ]; then
12 | echo "Service $1:$2 did not start within $TIMEOUT_LENGTH seconds. Aborting..."
13 | exit 1
14 | fi
15 | echo "sleeping"
16 | sleep $SLEEP_LENGTH
17 | done
18 | }
19 |
20 | for var in "$@"
21 | do
22 | host="${var%:*}"
23 | port="${var#*:}"
24 | wait_for "$host" "$port"
25 | done
26 |
--------------------------------------------------------------------------------
/docker/integration-wait.df:
--------------------------------------------------------------------------------
1 | FROM alpine:3.6
2 |
3 | ADD integration-test-wait.sh /usr/local/bin/entrypoint.sh
4 |
5 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
6 |
--------------------------------------------------------------------------------
/docker/integration_resources/client_setup.py:
--------------------------------------------------------------------------------
1 | from clutch import Client
2 |
3 | client = Client(host="transmission")
4 |
--------------------------------------------------------------------------------
/docker/integration_resources/watch/ion.torrent:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/docker/integration_resources/watch/ion.torrent
--------------------------------------------------------------------------------
/docker/integration_resources/watch/little_women.torrent:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/docker/integration_resources/watch/little_women.torrent
--------------------------------------------------------------------------------
/docker/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "rpc-enabled" : "true",
3 | "rpc-host-whitelist-enabled": true,
4 | "rpc-host-whitelist": "transmission",
5 | "rpc-whitelist-enabled": false,
6 | "watch-dir-enabled": true,
7 | "watch-dir": "/"
8 | }
9 |
--------------------------------------------------------------------------------
/docker/transmission.df:
--------------------------------------------------------------------------------
1 | FROM archlinux:latest
2 | RUN pacman -Syu --noconfirm
3 |
4 | ARG APPROOT=/app
5 | ENV APPROOT $APPROOT
6 |
7 | # Install transmission:
8 | RUN pacman -S --noconfirm transmission-cli
9 |
10 | WORKDIR $APPROOT
11 | COPY ./docker/settings.json $APPROOT
12 | COPY ./docker/integration_resources $APPROOT/integration_resources
13 | # exposes RPC port
14 | EXPOSE 9091
15 | # 51413/tcp 51413/udp
16 | ENTRYPOINT transmission-daemon --log-debug -f -c $APPROOT/integration_resources/watch -w $APPROOT/integration_resources/data -g $APPROOT
17 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/clutch.method.rst:
--------------------------------------------------------------------------------
1 | clutch.method package
2 | =====================
3 |
4 | Submodules
5 | ----------
6 |
7 | clutch.method.method module
8 | ---------------------------
9 |
10 | .. automodule:: clutch.method.method
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | clutch.method.misc module
16 | -------------------------
17 |
18 | .. automodule:: clutch.method.misc
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | clutch.method.queue module
24 | --------------------------
25 |
26 | .. automodule:: clutch.method.queue
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | clutch.method.session module
32 | ----------------------------
33 |
34 | .. automodule:: clutch.method.session
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | clutch.method.shared module
40 | ---------------------------
41 |
42 | .. automodule:: clutch.method.shared
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
47 | clutch.method.torrent module
48 | ----------------------------
49 |
50 | .. automodule:: clutch.method.torrent
51 | :members:
52 | :undoc-members:
53 | :show-inheritance:
54 |
55 | Module contents
56 | ---------------
57 |
58 | .. automodule:: clutch.method
59 | :members:
60 | :undoc-members:
61 | :show-inheritance:
62 |
--------------------------------------------------------------------------------
/docs/source/clutch.network.rpc.rst:
--------------------------------------------------------------------------------
1 | clutch.network.rpc package
2 | ==========================
3 |
4 | Submodules
5 | ----------
6 |
7 | clutch.network.rpc.convert module
8 | ---------------------------------
9 |
10 | .. automodule:: clutch.network.rpc.convert
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | clutch.network.rpc.message module
16 | ---------------------------------
17 |
18 | .. automodule:: clutch.network.rpc.message
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | Module contents
24 | ---------------
25 |
26 | .. automodule:: clutch.network.rpc
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
--------------------------------------------------------------------------------
/docs/source/clutch.network.rst:
--------------------------------------------------------------------------------
1 | clutch.network package
2 | ======================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 | :maxdepth: 4
9 |
10 | clutch.network.rpc
11 |
12 | Submodules
13 | ----------
14 |
15 | clutch.network.connection module
16 | --------------------------------
17 |
18 | .. automodule:: clutch.network.connection
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | clutch.network.session module
24 | -----------------------------
25 |
26 | .. automodule:: clutch.network.session
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | clutch.network.utility module
32 | -----------------------------
33 |
34 | .. automodule:: clutch.network.utility
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | Module contents
40 | ---------------
41 |
42 | .. automodule:: clutch.network
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
--------------------------------------------------------------------------------
/docs/source/clutch.rst:
--------------------------------------------------------------------------------
1 | clutch package
2 | ==============
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 | :maxdepth: 4
9 |
10 | clutch.method
11 | clutch.network
12 | clutch.schema
13 |
14 | Submodules
15 | ----------
16 |
17 | clutch.client module
18 | --------------------
19 |
20 | .. automodule:: clutch.client
21 | :members:
22 | :undoc-members:
23 | :show-inheritance:
24 |
25 | Module contents
26 | ---------------
27 |
28 | .. automodule:: clutch
29 | :members:
30 | :undoc-members:
31 | :show-inheritance:
32 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.request.misc.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.request.misc package
2 | ==================================
3 |
4 | Submodules
5 | ----------
6 |
7 | clutch.schema.request.misc.bandwidth\_group module
8 | --------------------------------------------------
9 |
10 | .. automodule:: clutch.schema.request.misc.bandwidth_group
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | clutch.schema.request.misc.port\_test module
16 | --------------------------------------------
17 |
18 | .. automodule:: clutch.schema.request.misc.port_test
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | Module contents
24 | ---------------
25 |
26 | .. automodule:: clutch.schema.request.misc
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.request.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.request package
2 | =============================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 | :maxdepth: 4
9 |
10 | clutch.schema.request.misc
11 | clutch.schema.request.session
12 | clutch.schema.request.torrent
13 |
14 | Module contents
15 | ---------------
16 |
17 | .. automodule:: clutch.schema.request
18 | :members:
19 | :undoc-members:
20 | :show-inheritance:
21 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.request.session.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.request.session package
2 | =====================================
3 |
4 | Submodules
5 | ----------
6 |
7 | clutch.schema.request.session.accessor module
8 | ---------------------------------------------
9 |
10 | .. automodule:: clutch.schema.request.session.accessor
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | clutch.schema.request.session.mutator module
16 | --------------------------------------------
17 |
18 | .. automodule:: clutch.schema.request.session.mutator
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | clutch.schema.request.session.shared module
24 | -------------------------------------------
25 |
26 | .. automodule:: clutch.schema.request.session.shared
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | Module contents
32 | ---------------
33 |
34 | .. automodule:: clutch.schema.request.session
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.request.torrent.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.request.torrent package
2 | =====================================
3 |
4 | Submodules
5 | ----------
6 |
7 | clutch.schema.request.torrent.accessor module
8 | ---------------------------------------------
9 |
10 | .. automodule:: clutch.schema.request.torrent.accessor
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | clutch.schema.request.torrent.add module
16 | ----------------------------------------
17 |
18 | .. automodule:: clutch.schema.request.torrent.add
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | clutch.schema.request.torrent.mutator module
24 | --------------------------------------------
25 |
26 | .. automodule:: clutch.schema.request.torrent.mutator
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | Module contents
32 | ---------------
33 |
34 | .. automodule:: clutch.schema.request.torrent
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.rst:
--------------------------------------------------------------------------------
1 | clutch.schema package
2 | =====================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 | :maxdepth: 4
9 |
10 | clutch.schema.request
11 | clutch.schema.user
12 |
13 | Module contents
14 | ---------------
15 |
16 | .. automodule:: clutch.schema
17 | :members:
18 | :undoc-members:
19 | :show-inheritance:
20 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.user.method.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.user.method package
2 | =================================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 | :maxdepth: 4
9 |
10 | clutch.schema.user.method.session
11 | clutch.schema.user.method.torrent
12 |
13 | Submodules
14 | ----------
15 |
16 | clutch.schema.user.method.misc module
17 | -------------------------------------
18 |
19 | .. automodule:: clutch.schema.user.method.misc
20 | :members:
21 | :undoc-members:
22 | :show-inheritance:
23 |
24 | clutch.schema.user.method.shared module
25 | ---------------------------------------
26 |
27 | .. automodule:: clutch.schema.user.method.shared
28 | :members:
29 | :undoc-members:
30 | :show-inheritance:
31 |
32 | Module contents
33 | ---------------
34 |
35 | .. automodule:: clutch.schema.user.method
36 | :members:
37 | :undoc-members:
38 | :show-inheritance:
39 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.user.method.session.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.user.method.session package
2 | =========================================
3 |
4 | Submodules
5 | ----------
6 |
7 | clutch.schema.user.method.session.accessor module
8 | -------------------------------------------------
9 |
10 | .. automodule:: clutch.schema.user.method.session.accessor
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | clutch.schema.user.method.session.mutator module
16 | ------------------------------------------------
17 |
18 | .. automodule:: clutch.schema.user.method.session.mutator
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | clutch.schema.user.method.session.shared module
24 | -----------------------------------------------
25 |
26 | .. automodule:: clutch.schema.user.method.session.shared
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | Module contents
32 | ---------------
33 |
34 | .. automodule:: clutch.schema.user.method.session
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.user.method.torrent.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.user.method.torrent package
2 | =========================================
3 |
4 | Submodules
5 | ----------
6 |
7 | clutch.schema.user.method.torrent.accessor module
8 | -------------------------------------------------
9 |
10 | .. automodule:: clutch.schema.user.method.torrent.accessor
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | clutch.schema.user.method.torrent.action module
16 | -----------------------------------------------
17 |
18 | .. automodule:: clutch.schema.user.method.torrent.action
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | clutch.schema.user.method.torrent.add module
24 | --------------------------------------------
25 |
26 | .. automodule:: clutch.schema.user.method.torrent.add
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | clutch.schema.user.method.torrent.move module
32 | ---------------------------------------------
33 |
34 | .. automodule:: clutch.schema.user.method.torrent.move
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | clutch.schema.user.method.torrent.mutator module
40 | ------------------------------------------------
41 |
42 | .. automodule:: clutch.schema.user.method.torrent.mutator
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
47 | clutch.schema.user.method.torrent.remove module
48 | -----------------------------------------------
49 |
50 | .. automodule:: clutch.schema.user.method.torrent.remove
51 | :members:
52 | :undoc-members:
53 | :show-inheritance:
54 |
55 | clutch.schema.user.method.torrent.rename module
56 | -----------------------------------------------
57 |
58 | .. automodule:: clutch.schema.user.method.torrent.rename
59 | :members:
60 | :undoc-members:
61 | :show-inheritance:
62 |
63 | Module contents
64 | ---------------
65 |
66 | .. automodule:: clutch.schema.user.method.torrent
67 | :members:
68 | :undoc-members:
69 | :show-inheritance:
70 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.user.response.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.user.response package
2 | ===================================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 | :maxdepth: 4
9 |
10 | clutch.schema.user.response.session
11 | clutch.schema.user.response.torrent
12 |
13 | Submodules
14 | ----------
15 |
16 | clutch.schema.user.response.misc module
17 | ---------------------------------------
18 |
19 | .. automodule:: clutch.schema.user.response.misc
20 | :members:
21 | :undoc-members:
22 | :show-inheritance:
23 |
24 | Module contents
25 | ---------------
26 |
27 | .. automodule:: clutch.schema.user.response
28 | :members:
29 | :undoc-members:
30 | :show-inheritance:
31 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.user.response.session.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.user.response.session package
2 | ===========================================
3 |
4 | Submodules
5 | ----------
6 |
7 | clutch.schema.user.response.session.accessor module
8 | ---------------------------------------------------
9 |
10 | .. automodule:: clutch.schema.user.response.session.accessor
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | clutch.schema.user.response.session.stats module
16 | ------------------------------------------------
17 |
18 | .. automodule:: clutch.schema.user.response.session.stats
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | Module contents
24 | ---------------
25 |
26 | .. automodule:: clutch.schema.user.response.session
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.user.response.torrent.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.user.response.torrent package
2 | ===========================================
3 |
4 | Submodules
5 | ----------
6 |
7 | clutch.schema.user.response.torrent.accessor module
8 | ---------------------------------------------------
9 |
10 | .. automodule:: clutch.schema.user.response.torrent.accessor
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | clutch.schema.user.response.torrent.add module
16 | ----------------------------------------------
17 |
18 | .. automodule:: clutch.schema.user.response.torrent.add
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | clutch.schema.user.response.torrent.rename module
24 | -------------------------------------------------
25 |
26 | .. automodule:: clutch.schema.user.response.torrent.rename
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | Module contents
32 | ---------------
33 |
34 | .. automodule:: clutch.schema.user.response.torrent
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
--------------------------------------------------------------------------------
/docs/source/clutch.schema.user.rst:
--------------------------------------------------------------------------------
1 | clutch.schema.user package
2 | ==========================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 | :maxdepth: 4
9 |
10 | clutch.schema.user.method
11 | clutch.schema.user.response
12 |
13 | Module contents
14 | ---------------
15 |
16 | .. automodule:: clutch.schema.user
17 | :members:
18 | :undoc-members:
19 | :show-inheritance:
20 |
--------------------------------------------------------------------------------
/docs/source/commands.rst:
--------------------------------------------------------------------------------
1 | Commands
2 | ========
3 |
4 | Transmission RPC specifies a variety of methods to call.
5 |
6 | In Clutch, those methods are implemented on :class:`~clutch.client.Client`.
7 |
8 | Groups
9 | ------
10 |
11 | Client methods are organized into groups: :attr:`~clutch.client.Client.torrent`, :attr:`~clutch.client.Client.session`, :attr:`~clutch.client.Client.queue`, :attr:`~clutch.client.Client.misc`.
12 |
13 | Torrent
14 | .......
15 | All torrent methods are found in :class:`~clutch.method.torrent.TorrentMethods`.
16 |
17 | Session
18 | .......
19 | All session methods are found in :class:`~clutch.method.session.SessionMethods`.
20 |
21 | Queue
22 | .....
23 | All queue methods are found in :class:`~clutch.method.queue.QueueMethods`.
24 |
25 | Misc
26 | ....
27 | All misc methods are found in :class:`~clutch.method.misc.MiscellaneousMethods`.
28 |
29 | Responses
30 | ---------
31 | Methods return a :class:`~clutch.network.rpc.message.Response` object that has three fields: ``result``, ``arguments`` and ``tag``.
32 |
33 | The :class:`~clutch.network.rpc.message.Response` object and :attr:`~clutch.network.rpc.message.Response.arguments` field are both `pydantic models`_.
34 |
35 | So they have some `useful methods for converting`_ into simple data formats like ``dict``:
36 |
37 | .. code-block:: python
38 |
39 | model.dict(...)
40 |
41 | Or a JSON string:
42 |
43 | .. code-block:: python
44 |
45 | model.json(...)
46 |
47 | To make a model with many fields more manageable, remove the clutter of empty fields using the option ``exclude_none``:
48 |
49 | .. code-block:: python
50 |
51 | model.dict(exclude_none=True)
52 |
53 | .. _pydantic models: https://pydantic-docs.helpmanual.io/usage/models/
54 | .. _useful methods for converting: https://pydantic-docs.helpmanual.io/usage/exporting_models/
55 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 | import os
6 | import sys
7 |
8 | # -- Project information -----------------------------------------------------
9 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
10 |
11 |
12 | sys.path.insert(0, os.path.abspath("../../"))
13 |
14 |
15 | project = 'Clutch'
16 | copyright = '2025, Michael Hadam'
17 | author = 'Michael Hadam'
18 | release = '7.0.3'
19 |
20 | # -- General configuration ---------------------------------------------------
21 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
22 |
23 | extensions = [
24 | "sphinx.ext.autodoc",
25 | "sphinx_autodoc_typehints",
26 | ]
27 |
28 | always_document_param_types = True
29 | set_type_checking_flag = True
30 |
31 | templates_path = ['_templates']
32 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
33 |
34 |
35 |
36 | # -- Options for HTML output -------------------------------------------------
37 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
38 |
39 | html_theme = 'alabaster'
40 | html_static_path = ['_static']
41 |
--------------------------------------------------------------------------------
/docs/source/examples.rst:
--------------------------------------------------------------------------------
1 | Examples
2 | --------
3 |
4 | Create a client
5 | ===============
6 | .. code-block:: python
7 |
8 | client = Client()
9 |
10 | List all fields of current torrents
11 | ===================================
12 |
13 | .. code-block:: python
14 |
15 | response: Response[TorrentAccessorResponse] = client.torrent.accessor(all_fields=True)
16 | torrents: Sequence[TorrentAccessorObject] = response.arguments.torrents
17 | print(torrents[0].dict(exclude_none=True))
18 |
19 | Get specific fields of torrents
20 | ===============================
21 |
22 | .. code-block:: python
23 |
24 | fields: Set[TorrentAccessorField] = {"id", "status", "name"}
25 | response: Response[TorrentAccessorResponse] = client.torrent.accessor(fields)
26 | torrents: Sequence[TorrentAccessorObject] = response.arguments.torrents
27 | torrent = torrents[0]
28 | torrent_id, torrent_status, torrent_name = torrent.id, torrent.status, torrent.name
29 |
30 | Remove torrents
31 | ===============
32 |
33 | .. code-block:: python
34 |
35 | response: Response[TorrentAccessorResponse] = client.torrent.remove(
36 | torrent_id, delete_local_data=False
37 | )
38 |
39 | Add torrents
40 | ===============
41 |
42 | .. code-block:: python
43 |
44 | arguments: TorrentAddArguments = {
45 | "filename": "/path/to/file",
46 | "paused": True,
47 | }
48 | response: Response[TorrentAdd] = client.torrent.add(arguments)
49 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. meta::
2 | :description: Transmission RPC library used to control the Transmission BitTorrent client.
3 |
4 | .. title:: Transmission RPC Library: Clutch
5 |
6 | Clutch - Transmission RPC for Python
7 | ====================================
8 |
9 | Version |release|.
10 |
11 | .. image:: https://img.shields.io/github/stars/mhadam/clutch?style=social
12 | :target: https://github.com/mhadam/clutch
13 | :alt: GitHub stars badge
14 |
15 | .. image:: https://img.shields.io/pypi/v/transmission-clutch.svg
16 | :target: https://pypi.org/project/transmission-clutch
17 | :alt: PyPI badge
18 |
19 | -----
20 |
21 | .. toctree::
22 | :hidden:
23 | :maxdepth: 2
24 | :caption: Contents:
25 |
26 | intro
27 | examples
28 | server_config
29 | commands
30 | modules
31 |
32 | Clutch is an open-source Python library for talking to the `Transmission BitTorrent client`_ over RPC_.
33 |
34 | It's handy for doing things like:
35 |
36 | * automating work when managing torrents
37 |
38 | * user interfaces and clients
39 |
40 | * collecting torrent stats
41 |
42 | Installation
43 | ------------
44 |
45 | Pip
46 | ***
47 | .. code-block:: console
48 |
49 | $ pip install transmission-clutch
50 |
51 | Poetry
52 | ******
53 | .. code-block:: console
54 |
55 | $ poetry add transmission-clutch
56 |
57 | Get started
58 | -------------
59 |
60 | Import the package and make a client:
61 |
62 | .. code-block:: python
63 |
64 | from clutch import Client
65 | client = Client(address="http://localhost:9091/transmission/rpc")
66 |
67 | Now issue a command to Transmission:
68 |
69 | .. code-block:: python
70 |
71 | >>> client.torrent.accessor(fields=['id', 'files'], ids=[1]).dict(exclude_none=True)
72 | {'result': 'success', 'arguments': {'torrents': [{'files': [{'bytes_completed': 1053440, 'length': 1053440, 'name': 'little_women/little_women.txt'}], 'id': 1}]}}
73 | >>> client.torrent.accessor(fields=['id', 'files'], ids=[1]).json(exclude_none=True)
74 | '{"result": "success", "arguments": {"torrents": [{"files": [{"bytes_completed": 1053440, "length": 1053440, "name": "little_women/little_women.txt"}], "id": 1}]}}'
75 |
76 | .. _RPC: https://en.wikipedia.org/wiki/Remote_procedure_call
77 | .. _`Transmission BitTorrent client`: https://transmissionbt.com
78 |
--------------------------------------------------------------------------------
/docs/source/intro.rst:
--------------------------------------------------------------------------------
1 | Intro
2 | =====
3 |
4 | Transmission comes with an `RPC server`_ with this `RPC protocol specification`_.
5 |
6 | When it receives a request, the server performs the requested action and sends back a response.
7 |
8 | RPC format
9 | ----------
10 |
11 | To illustrate the form of request and response messages, the following parts contain some pseudo-JSON schema.
12 |
13 | Request
14 | *******
15 |
16 | A request requires ``method`` be specified, ``arguments`` and ``tag`` are optional.
17 |
18 | ``tag`` can be provided for matching requests and responses.
19 |
20 | When the server responds, it includes the same ``tag`` that was provided in the request:
21 |
22 | .. code-block:: none
23 |
24 | {
25 | "method": str
26 | "arguments": Dict[str, object]
27 | "tag": int
28 | }
29 |
30 | Response
31 | ********
32 |
33 | Likewise, ``result`` is the only required part.
34 |
35 | .. code-block:: none
36 |
37 | {
38 | "result": str ("success" on success otherwise an error message)
39 | "arguments": Dict[str, object]
40 | "tag": int
41 | }
42 |
43 | .. _`RPC server`: https://en.wikipedia.org/wiki/Remote_procedure_call
44 | .. _`RPC protocol specification`: https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md
45 |
--------------------------------------------------------------------------------
/docs/source/modules.rst:
--------------------------------------------------------------------------------
1 | clutch
2 | ======
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | clutch
8 |
--------------------------------------------------------------------------------
/docs/source/requirements.txt:
--------------------------------------------------------------------------------
1 | # This file was autogenerated by uv via the following command:
2 | # uv export --only-group=docs --no-hashes
3 | alabaster==0.7.16
4 | anyio==4.8.0
5 | babel==2.16.0
6 | certifi==2024.12.14
7 | charset-normalizer==3.4.0
8 | click==8.1.7
9 | colorama==0.4.6
10 | docutils==0.21.2
11 | exceptiongroup==1.2.2
12 | h11==0.14.0
13 | idna==3.10
14 | imagesize==1.4.1
15 | jinja2==3.1.4
16 | markupsafe==3.0.2
17 | packaging==24.2
18 | pygments==2.18.0
19 | requests==2.32.3
20 | sniffio==1.3.1
21 | snowballstemmer==2.2.0
22 | sphinx==7.4.7
23 | sphinx-autobuild==2024.10.3
24 | sphinx-autodoc-typehints==2.3.0
25 | sphinxcontrib-applehelp==2.0.0
26 | sphinxcontrib-devhelp==2.0.0
27 | sphinxcontrib-htmlhelp==2.1.0
28 | sphinxcontrib-jsmath==1.0.1
29 | sphinxcontrib-qthelp==2.0.0
30 | sphinxcontrib-serializinghtml==2.0.0
31 | starlette==0.45.2
32 | tomli==2.2.1
33 | typing-extensions==4.12.2
34 | urllib3==2.2.3
35 | uvicorn==0.34.0
36 | watchfiles==1.0.4
37 | websockets==14.2
38 |
--------------------------------------------------------------------------------
/docs/source/server_config.rst:
--------------------------------------------------------------------------------
1 | Configuration
2 | -------------
3 | By default, the RPC server listens at::
4 |
5 | http://localhost:9091/transmission/rpc
6 |
7 | But this can be changed through configuration settings like ``rpc-bind-address`` and ``port``.
8 |
9 | Other useful settings include ``rpc-whitelist`` (for whitelisting IP addresses), and ``rpc-host-whitelist`` (for whitelisting domain names).
10 |
11 | GUI configuration
12 | *****************
13 |
14 | MacOS client
15 | ............
16 | In the MacOS desktop client, these settings are accessed through the system menu bar:
17 |
18 | :menuselection:`Transmission --> Preferences --> Remote`
19 |
20 | File configuration
21 | ******************
22 | Settings can be manually specified in a platform-specific file, as well.
23 |
24 | Reference `Transmission's configuration file documentation`_ for all available settings, their default values, as well as where the configuration file is located for each platform.
25 |
26 | .. _`Transmission's configuration file documentation`: https://github.com/transmission/transmission/wiki/Editing-Configuration-Files
27 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "transmission-clutch"
3 | version = "7.0.3"
4 | description = "An RPC client library for the Transmission BitTorrent client"
5 | authors = [
6 | { name = "Michael Hadam", email = "michael@hadam.us" }
7 | ]
8 | maintainers = [
9 | { name = "Michael Hadam", email = "michael@hadam.us" }
10 | ]
11 | readme = "README.rst"
12 | license = "MIT"
13 | license-files = ["LICEN[CS]E*"]
14 | packages = [{ include = "clutch" }]
15 | requires-python = ">=3.10"
16 | dependencies = [
17 | "requests~=2.32",
18 | "pydantic~=2.10"
19 | ]
20 |
21 | [project.urls]
22 | Documentation = "https://clutch.readthedocs.io/en/latest/"
23 | Repository = "https://github.com/mhadam/clutch"
24 | Issues = "https://github.com/mhadam/clutch/issues"
25 |
26 | [dependency-groups]
27 | test = [
28 | "pytest-httpserver>=1.1.0",
29 | "pytest~=8.3",
30 | "deepdiff>=8.1.1",
31 | "dirty-equals>=0.9.0",
32 | ]
33 | dev = [
34 | "ruff",
35 | "mypy",
36 | "hatchling>=1.27.0",
37 | "tomlscript",
38 | "types-requests>=2.32.0.20241016",
39 | ]
40 | docs = [
41 | "pydantic",
42 | "sphinx",
43 | "sphinx-autodoc-typehints",
44 | "sphinx-autobuild",
45 | ]
46 |
47 | [build-system]
48 | requires = ["hatchling"]
49 | build-backend = "hatchling.build"
50 |
51 | [tool.hatch.build.targets.wheel]
52 | packages = ["clutch"]
53 |
54 | [tool.pytest.ini_options]
55 | log_cli = true
56 | log_cli_level = "INFO"
57 | log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
58 | log_cli_date_format = "%Y-%m-%d %H:%M:%S"
59 |
60 | [tool.ruff]
61 | target-version = "py310"
62 | lint.extend-select = ["RUF013", "F401"]
63 | extend-include = ["clutch/", "tests/"]
64 |
65 | [tool.mypy]
66 | plugins = ["pydantic.mypy"]
67 |
68 | [tool.tomlscript]
69 | docs-autodoc = "uv run sphinx-apidoc -o source/ ../clutch"
70 | docs-autogen = "uv run sphinx-autogen source/index.rst -o source/"
71 | docs-reqs = "uv export --only-group=docs --no-hashes | awk '{print $1}' FS=' ;' > docs/requirements.txt"
72 | mypy = "uv run mypy clutch/ tests/"
73 | ruff = "ruff format"
74 | unit = """
75 | docker build -f docker/clutch.df -t clutch-test .
76 | docker run --rm --entrypoint "/bin/sh" clutch-test -c "mypy .; pytest tests/unit"
77 | """
78 | docs-gen = "uv run sphinx-autogen docs/index.rst -o docs/"
79 | docs-live = "uv run sphinx-autobuild docs docs/_build/html"
80 | clean-containers = """
81 | docker stop $(docker ps -a -q)
82 | docker rm $(docker ps -a -q)
83 | """
84 | integration-shell = """
85 | docker-compose -f ./docker/docker-compose.yml up -d --force-recreate --no-deps --build testbed transmission
86 | docker-compose -f ./docker/docker-compose.yml run --rm start_dependencies
87 | docker-compose -f ./docker/docker-compose.yml run --rm testbed sh -c "python -i client_setup.py"
88 | """
89 | end-to-end = """
90 | docker-compose -f ./docker/docker-compose.yml up -d --force-recreate --no-deps --build testbed transmission
91 | docker-compose -f ./docker/docker-compose.yml run --rm start_dependencies
92 | docker-compose -f ./docker/docker-compose.yml run --rm testbed sh -c "pytest tests/endtoend"
93 | """
94 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # This file was autogenerated by uv via the following command:
2 | # uv pip compile pyproject.toml -o requirements.txt
3 | annotated-types==0.7.0
4 | # via pydantic
5 | certifi==2024.12.14
6 | # via requests
7 | charset-normalizer==3.4.1
8 | # via requests
9 | idna==3.10
10 | # via requests
11 | pydantic==2.10.5
12 | # via transmission-clutch (pyproject.toml)
13 | pydantic-core==2.27.2
14 | # via pydantic
15 | requests==2.32.3
16 | # via transmission-clutch (pyproject.toml)
17 | typing-extensions==4.12.2
18 | # via
19 | # pydantic
20 | # pydantic-core
21 | urllib3==2.3.0
22 | # via requests
23 |
--------------------------------------------------------------------------------
/rpc-spec.md:
--------------------------------------------------------------------------------
1 | # Transmission's RPC specification
2 | This document describes a protocol for interacting with Transmission sessions remotely.
3 |
4 | ### 1.1 Terminology
5 | The [JSON](https://www.json.org/) terminology in [RFC 4627](https://datatracker.ietf.org/doc/html/rfc4627) is used.
6 | RPC requests and responses are formatted in JSON.
7 |
8 | ### 1.2 Tools
9 | If `transmission-remote` is called with a `--debug` argument, its RPC traffic to the Transmission server will be dumped to the terminal. This can be useful when you want to compare requests in your application to another for reference.
10 |
11 | If `transmission-qt` is run with an environment variable `TR_RPC_VERBOSE` set, it too will dump the RPC requests and responses to the terminal for inspection.
12 |
13 | Lastly, using the browser's developer tools in the Transmission web client is always an option.
14 |
15 | ### 1.3 Libraries of ready-made wrappers
16 | Some people outside of the Transmission project have written libraries that wrap this RPC API. These aren't supported by the Transmission project, but are listed here in the hope that they may be useful:
17 |
18 | | Language | Link
19 | |:---|:---
20 | | C# | https://www.nuget.org/packages/Transmission.API.RPC
21 | | Go | https://github.com/hekmon/transmissionrpc
22 | | Python | https://github.com/Trim21/transmission-rpc
23 | | Rust | https://crates.io/crates/transmission-rpc
24 |
25 |
26 | ## 2 Message format
27 | Messages are formatted as objects. There are two types: requests (described in [section 2.1](#21-requests)) and responses (described in [section 2.2](#22-responses)).
28 |
29 | All text **must** be UTF-8 encoded.
30 |
31 | ### 2.1 Requests
32 | Requests support three keys:
33 |
34 | 1. A required `method` string telling the name of the method to invoke
35 | 2. An optional `arguments` object of key/value pairs. The keys allowed are defined by the `method`.
36 | 3. An optional `tag` number used by clients to track responses. If provided by a request, the response MUST include the same tag.
37 |
38 | ```json
39 | {
40 | "arguments": {
41 | "fields": [
42 | "version"
43 | ]
44 | },
45 | "method": "session-get",
46 | "tag": 912313
47 | }
48 | ```
49 |
50 |
51 | ### 2.2 Responses
52 | Responses to a request will include:
53 |
54 | 1. A required `result` string whose value MUST be `success` on success, or an error string on failure.
55 | 2. An optional `arguments` object of key/value pairs. Its keys contents are defined by the `method` and `arguments` of the original request.
56 | 3. An optional `tag` number as described in 2.1.
57 |
58 | ```json
59 | {
60 | "arguments": {
61 | "version": "2.93 (3c5870d4f5)"
62 | },
63 | "result": "success",
64 | "tag": 912313
65 | }
66 | ```
67 |
68 | ### 2.3 Transport mechanism
69 | HTTP POSTing a JSON-encoded request is the preferred way of communicating
70 | with a Transmission RPC server. The current Transmission implementation
71 | has the default URL as `http://host:9091/transmission/rpc`. Clients
72 | may use this as a default, but should allow the URL to be reconfigured,
73 | since the port and path may be changed to allow mapping and/or multiple
74 | daemons to run on a single server.
75 |
76 | #### 2.3.1 CSRF protection
77 | Most Transmission RPC servers require a `X-Transmission-Session-Id`
78 | header to be sent with requests, to prevent CSRF attacks.
79 |
80 | When your request has the wrong id -- such as when you send your first
81 | request, or when the server expires the CSRF token -- the
82 | Transmission RPC server will return an HTTP 409 error with the
83 | right `X-Transmission-Session-Id` in its own headers.
84 |
85 | So, the correct way to handle a 409 response is to update your
86 | `X-Transmission-Session-Id` and to resend the previous request.
87 |
88 | #### 2.3.2 DNS rebinding protection
89 | Additional check is being made on each RPC request to make sure that the
90 | client sending the request does so using one of the allowed hostnames by
91 | which RPC server is meant to be available.
92 |
93 | If host whitelisting is enabled (which is true by default), Transmission
94 | inspects the `Host:` HTTP header value (with port stripped, if any) and
95 | matches it to one of the whitelisted names. Regardless of host whitelist
96 | content, `localhost` and `localhost.` domain names as well as all the IP
97 | addresses are always implicitly allowed.
98 |
99 | For more information on configuration, see settings.json documentation for
100 | `rpc-host-whitelist-enabled` and `rpc-host-whitelist` keys.
101 |
102 | #### 2.3.3 Authentication
103 | Enabling authentication is an optional security feature that can be enabled
104 | on Transmission RPC servers. Authentication occurs by method of HTTP Basic
105 | Access Authentication.
106 |
107 | If authentication is enabled, Transmission inspects the `Authorization:`
108 | HTTP header value to validate the credentials of the request. The value
109 | of this HTTP header is expected to be [`Basic `](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization#basic),
110 | where is equal to a base64 encoded string of the
111 | username and password (respectively), separated by a colon.
112 |
113 | ## 3 Torrent requests
114 | ### 3.1 Torrent action requests
115 | | Method name | libtransmission function | Description
116 | |:--|:--|:--
117 | | `torrent-start` | tr_torrentStart | start torrent
118 | | `torrent-start-now` | tr_torrentStartNow | start torrent disregarding queue position
119 | | `torrent-stop` | tr_torrentStop | stop torrent
120 | | `torrent-verify` | tr_torrentVerify | verify torrent
121 | | `torrent-reannounce` | tr_torrentManualUpdate | re-announce to trackers now
122 |
123 | Request arguments: `ids`, which specifies which torrents to use.
124 | All torrents are used if the `ids` argument is omitted.
125 |
126 | `ids` should be one of the following:
127 |
128 | 1. an integer referring to a torrent id
129 | 2. a list of torrent id numbers, SHA1 hash strings, or both
130 | 3. a string, `recently-active`, for recently-active torrents
131 |
132 | Note that integer torrent ids are not stable across Transmission daemon
133 | restarts. Use torrent hashes if you need stable ids.
134 |
135 | Response arguments: none
136 |
137 | ### 3.2 Torrent mutator: `torrent-set`
138 | Method name: `torrent-set`
139 |
140 | Request arguments:
141 |
142 | | Key | Value Type | Value Description
143 | |:--|:--|:--
144 | | `bandwidthPriority` | number | this torrent's bandwidth tr_priority_t
145 | | `downloadLimit` | number | maximum download speed (KBps)
146 | | `downloadLimited` | boolean | true if `downloadLimit` is honored
147 | | `files-unwanted` | array | indices of file(s) to not download
148 | | `files-wanted` | array | indices of file(s) to download
149 | | `group` | string | The name of this torrent's bandwidth group
150 | | `honorsSessionLimits` | boolean | true if session upload limits are honored
151 | | `ids` | array | torrent list, as described in 3.1
152 | | `labels` | array | array of string labels
153 | | `location` | string | new location of the torrent's content
154 | | `peer-limit` | number | maximum number of peers
155 | | `priority-high` | array | indices of high-priority file(s)
156 | | `priority-low` | array | indices of low-priority file(s)
157 | | `priority-normal` | array | indices of normal-priority file(s)
158 | | `queuePosition` | number | position of this torrent in its queue [0...n)
159 | | `seedIdleLimit` | number | torrent-level number of minutes of seeding inactivity
160 | | `seedIdleMode` | number | which seeding inactivity to use. See tr_idlelimit
161 | | `seedRatioLimit` | double | torrent-level seeding ratio
162 | | `seedRatioMode` | number | which ratio to use. See tr_ratiolimit
163 | | `sequentialDownload` | boolean | download torrent pieces sequentially
164 | | `trackerAdd` | array | **DEPRECATED** use trackerList instead
165 | | `trackerList` | string | string of announce URLs, one per line, and a blank line between [tiers](https://www.bittorrent.org/beps/bep_0012.html).
166 | | `trackerRemove` | array | **DEPRECATED** use trackerList instead
167 | | `trackerReplace` | array | **DEPRECATED** use trackerList instead
168 | | `uploadLimit` | number | maximum upload speed (KBps)
169 | | `uploadLimited` | boolean | true if `uploadLimit` is honored
170 |
171 | Just as an empty `ids` value is shorthand for "all ids", using an empty array
172 | for `files-wanted`, `files-unwanted`, `priority-high`, `priority-low`, or
173 | `priority-normal` is shorthand for saying "all files".
174 |
175 | Response arguments: none
176 |
177 | ### 3.3 Torrent accessor: `torrent-get`
178 | Method name: `torrent-get`.
179 |
180 | Request arguments:
181 |
182 | 1. An optional `ids` array as described in 3.1.
183 | 2. A required `fields` array of keys. (see list below)
184 | 3. An optional `format` string specifying how to format the
185 | `torrents` response field. Allowed values are `objects`
186 | (default) and `table`. (see "Response arguments" below)
187 |
188 | Response arguments:
189 |
190 | 1. A `torrents` array.
191 |
192 | If the `format` request was `objects` (default), `torrents` will
193 | be an array of objects, each of which contains the key/value
194 | pairs matching the request's `fields` arg. This was the only
195 | format before Transmission 3 and has some obvious programmer
196 | conveniences, such as parsing directly into Javascript objects.
197 |
198 | If the format was `table`, then `torrents` will be an array of
199 | arrays. The first row holds the keys and each remaining row holds
200 | a torrent's values for those keys. This format is more efficient
201 | in terms of JSON generation and JSON parsing.
202 |
203 | 2. If the request's `ids` field was `recently-active`,
204 | a `removed` array of torrent-id numbers of recently-removed
205 | torrents.
206 |
207 | Note: For more information on what these fields mean, see the comments
208 | in [libtransmission/transmission.h](../libtransmission/transmission.h).
209 | The 'source' column here corresponds to the data structure there.
210 |
211 | | Key | Value Type | transmission.h source
212 | |:--|:--|:--
213 | | `activityDate` | number | tr_stat
214 | | `addedDate` | number | tr_stat
215 | | `availability` | array (see below)| tr_torrentAvailability()
216 | | `bandwidthPriority` | number | tr_priority_t
217 | | `comment` | string | tr_torrent_view
218 | | `corruptEver`| number | tr_stat
219 | | `creator`| string | tr_torrent_view
220 | | `dateCreated`| number| tr_torrent_view
221 | | `desiredAvailable`| number| tr_stat
222 | | `doneDate`| number | tr_stat
223 | | `downloadDir` | string | tr_torrent
224 | | `downloadedEver` | number | tr_stat
225 | | `downloadLimit` | number | tr_torrent
226 | | `downloadLimited` | boolean | tr_torrent
227 | | `editDate` | number | tr_stat
228 | | `error` | number | tr_stat
229 | | `errorString` | string | tr_stat
230 | | `eta` | number | tr_stat
231 | | `etaIdle` | number | tr_stat
232 | | `file-count` | number | tr_info
233 | | `files`| array (see below)| n/a
234 | | `fileStats`| array (see below)| n/a
235 | | `group`| string| n/a
236 | | `hashString`| string| tr_torrent_view
237 | | `haveUnchecked`| number| tr_stat
238 | | `haveValid`| number| tr_stat
239 | | `honorsSessionLimits`| boolean| tr_torrent
240 | | `id` | number | tr_torrent
241 | | `isFinished` | boolean| tr_stat
242 | | `isPrivate` | boolean| tr_torrent
243 | | `isStalled` | boolean| tr_stat
244 | | `labels` | array of strings | tr_torrent
245 | | `leftUntilDone` | number| tr_stat
246 | | `magnetLink` | string| n/a
247 | | `manualAnnounceTime` | number| tr_stat
248 | | `maxConnectedPeers` | number| tr_torrent
249 | | `metadataPercentComplete` | double| tr_stat
250 | | `name` | string| tr_torrent_view
251 | | `peer-limit` | number| tr_torrent
252 | | `peers` | array (see below)| n/a
253 | | `peersConnected` | number| tr_stat
254 | | `peersFrom` | object (see below)| n/a
255 | | `peersGettingFromUs` | number| tr_stat
256 | | `peersSendingToUs` | number| tr_stat
257 | | `percentComplete` | double | tr_stat
258 | | `percentDone` | double | tr_stat
259 | | `pieces` | string (see below)| tr_torrent
260 | | `pieceCount`| number| tr_torrent_view
261 | | `pieceSize`| number| tr_torrent_view
262 | | `priorities`| array (see below)| n/a
263 | | `primary-mime-type`| string| tr_torrent
264 | | `queuePosition`| number| tr_stat
265 | | `rateDownload` (B/s)| number| tr_stat
266 | | `rateUpload` (B/s)| number| tr_stat
267 | | `recheckProgress`| double| tr_stat
268 | | `secondsDownloading`| number| tr_stat
269 | | `secondsSeeding`| number| tr_stat
270 | | `seedIdleLimit`| number| tr_torrent
271 | | `seedIdleMode`| number| tr_inactivelimit
272 | | `seedRatioLimit`| double| tr_torrent
273 | | `seedRatioMode`| number| tr_ratiolimit
274 | | `sequentialDownload`| boolean| tr_torrent
275 | | `sizeWhenDone`| number| tr_stat
276 | | `startDate`| number| tr_stat
277 | | `status`| number (see below)| tr_stat
278 | | `trackers`| array (see below)| n/a
279 | | `trackerList` | string | string of announce URLs, one per line, with a blank line between tiers
280 | | `trackerStats`| array (see below)| n/a
281 | | `totalSize`| number| tr_torrent_view
282 | | `torrentFile`| string| tr_info
283 | | `uploadedEver`| number| tr_stat
284 | | `uploadLimit`| number| tr_torrent
285 | | `uploadLimited`| boolean| tr_torrent
286 | | `uploadRatio`| double| tr_stat
287 | | `wanted`| array (see below)| n/a
288 | | `webseeds`| array of strings | tr_tracker_view
289 | | `webseedsSendingToUs`| number| tr_stat
290 |
291 | `availability`: An array of `pieceCount` numbers representing the number of connected peers that have each piece, or -1 if we already have the piece ourselves.
292 |
293 | `files`: array of objects, each containing:
294 |
295 | | Key | Value Type | transmission.h source
296 | |:--|:--|:--
297 | | `bytesCompleted` | number | tr_file_view
298 | | `length` | number | tr_file_view
299 | | `name` | string | tr_file_view
300 | | `beginPiece` | number | tr_file_view
301 | | `endPiece` | number | tr_file_view
302 |
303 | Files are returned in the order they are laid out in the torrent. References to "file indices" throughout this specification should be interpreted as the position of the file within this ordering, with the first file bearing index 0.
304 |
305 | `fileStats`: a file's non-constant properties. An array of `tr_info.filecount` objects, in the same order as `files`, each containing:
306 |
307 | | Key | Value Type | transmission.h source
308 | |:--|:--|:--
309 | | `bytesCompleted` | number | tr_file_view
310 | | `wanted` | boolean | tr_file_view (**Note:** Not to be confused with `torrent-get.wanted`, which is an array of 0/1 instead of boolean)
311 | | `priority` | number | tr_file_view
312 |
313 | `peers`: an array of objects, each containing:
314 |
315 | | Key | Value Type | transmission.h source
316 | |:--|:--|:--
317 | | `address` | string | tr_peer_stat
318 | | `clientName` | string | tr_peer_stat
319 | | `clientIsChoked` | boolean | tr_peer_stat
320 | | `clientIsInterested` | boolean | tr_peer_stat
321 | | `flagStr` | string | tr_peer_stat
322 | | `isDownloadingFrom` | boolean | tr_peer_stat
323 | | `isEncrypted` | boolean | tr_peer_stat
324 | | `isIncoming` | boolean | tr_peer_stat
325 | | `isUploadingTo` | boolean | tr_peer_stat
326 | | `isUTP` | boolean | tr_peer_stat
327 | | `peerIsChoked` | boolean | tr_peer_stat
328 | | `peerIsInterested` | boolean | tr_peer_stat
329 | | `port` | number | tr_peer_stat
330 | | `progress` | double | tr_peer_stat
331 | | `rateToClient` (B/s) | number | tr_peer_stat
332 | | `rateToPeer` (B/s) | number | tr_peer_stat
333 |
334 | `peersFrom`: an object containing:
335 |
336 | | Key | Value Type | transmission.h source
337 | |:--|:--|:--
338 | | `fromCache` | number | tr_stat
339 | | `fromDht` | number | tr_stat
340 | | `fromIncoming` | number | tr_stat
341 | | `fromLpd` | number | tr_stat
342 | | `fromLtep` | number | tr_stat
343 | | `fromPex` | number | tr_stat
344 | | `fromTracker` | number | tr_stat
345 |
346 |
347 | `pieces`: A bitfield holding pieceCount flags which are set to 'true' if we have the piece matching that position. JSON doesn't allow raw binary data, so this is a base64-encoded string. (Source: tr_torrent)
348 |
349 | `priorities`: An array of `tr_torrentFileCount()` numbers. Each is the `tr_priority_t` mode for the corresponding file.
350 |
351 | `status`: A number between 0 and 6, where:
352 |
353 | | Value | Meaning
354 | |:--|:--
355 | | 0 | Torrent is stopped
356 | | 1 | Torrent is queued to verify local data
357 | | 2 | Torrent is verifying local data
358 | | 3 | Torrent is queued to download
359 | | 4 | Torrent is downloading
360 | | 5 | Torrent is queued to seed
361 | | 6 | Torrent is seeding
362 |
363 |
364 | `trackers`: array of objects, each containing:
365 |
366 | | Key | Value Type | transmission.h source
367 | |:--|:--|:--
368 | | `announce` | string | tr_tracker_view
369 | | `id` | number | tr_tracker_view
370 | | `scrape` | string | tr_tracker_view
371 | | `sitename` | string | tr_tracker_view
372 | | `tier` | number | tr_tracker_view
373 |
374 | `trackerStats`: array of objects, each containing:
375 |
376 | | Key | Value Type | transmission.h source
377 | |:--|:--|:--
378 | | `announce` | string | tr_tracker_view
379 | | `announceState` | number | tr_tracker_view
380 | | `downloadCount` | number | tr_tracker_view
381 | | `hasAnnounced` | boolean | tr_tracker_view
382 | | `hasScraped` | boolean | tr_tracker_view
383 | | `host` | string | tr_tracker_view
384 | | `id` | number | tr_tracker_view
385 | | `isBackup` | boolean | tr_tracker_view
386 | | `lastAnnouncePeerCount` | number | tr_tracker_view
387 | | `lastAnnounceResult` | string | tr_tracker_view
388 | | `lastAnnounceStartTime` | number | tr_tracker_view
389 | | `lastAnnounceSucceeded` | boolean | tr_tracker_view
390 | | `lastAnnounceTime` | number | tr_tracker_view
391 | | `lastAnnounceTimedOut` | boolean | tr_tracker_view
392 | | `lastScrapeResult` | string | tr_tracker_view
393 | | `lastScrapeStartTime` | number | tr_tracker_view
394 | | `lastScrapeSucceeded` | boolean | tr_tracker_view
395 | | `lastScrapeTime` | number | tr_tracker_view
396 | | `lastScrapeTimedOut` | boolean | tr_tracker_view
397 | | `leecherCount` | number | tr_tracker_view
398 | | `nextAnnounceTime` | number | tr_tracker_view
399 | | `nextScrapeTime` | number | tr_tracker_view
400 | | `scrape` | string | tr_tracker_view
401 | | `scrapeState` | number | tr_tracker_view
402 | | `seederCount` | number | tr_tracker_view
403 | | `sitename` | string | tr_tracker_view
404 | | `tier` | number | tr_tracker_view
405 |
406 |
407 | `wanted`: An array of `tr_torrentFileCount()` 0/1, 1 (true) if the corresponding file is to be downloaded. (Source: `tr_file_view`)
408 |
409 | **Note:** For backwards compatibility, in `4.x.x`, `wanted` is serialized as an array of `0` or `1` that should be treated as booleans.
410 | This will be fixed in `5.0.0` to return an array of booleans.
411 |
412 | Example:
413 |
414 | Say we want to get the name and total size of torrents #7 and #10.
415 |
416 | Request:
417 |
418 | ```json
419 | {
420 | "arguments": {
421 | "fields": [ "id", "name", "totalSize" ],
422 | "ids": [ 7, 10 ]
423 | },
424 | "method": "torrent-get",
425 | "tag": 39693
426 | }
427 | ```
428 |
429 | Response:
430 |
431 | ```json
432 | {
433 | "arguments": {
434 | "torrents": [
435 | {
436 | "id": 10,
437 | "name": "Fedora x86_64 DVD",
438 | "totalSize": 34983493932
439 | },
440 | {
441 | "id": 7,
442 | "name": "Ubuntu x86_64 DVD",
443 | "totalSize": 9923890123
444 | }
445 | ]
446 | },
447 | "result": "success",
448 | "tag": 39693
449 | }
450 | ```
451 |
452 | ### 3.4 Adding a torrent
453 | Method name: `torrent-add`
454 |
455 | Request arguments:
456 |
457 | | Key | Value Type | Description
458 | |:--|:--|:--
459 | | `cookies` | string | pointer to a string of one or more cookies.
460 | | `download-dir` | string | path to download the torrent to
461 | | `filename` | string | filename or URL of the .torrent file
462 | | `labels` | array | array of string labels
463 | | `metainfo` | string | base64-encoded .torrent content
464 | | `paused` | boolean | if true, don't start the torrent
465 | | `peer-limit` | number | maximum number of peers
466 | | `bandwidthPriority` | number | torrent's bandwidth tr_priority_t
467 | | `files-wanted` | array | indices of file(s) to download
468 | | `files-unwanted` | array | indices of file(s) to not download
469 | | `priority-high` | array | indices of high-priority file(s)
470 | | `priority-low` | array | indices of low-priority file(s)
471 | | `priority-normal` | array | indices of normal-priority file(s)
472 |
473 | Either `filename` **or** `metainfo` **must** be included. All other arguments are optional.
474 |
475 | The format of the `cookies` should be `NAME=CONTENTS`, where `NAME` is the cookie name and `CONTENTS` is what the cookie should contain. Set multiple cookies like this: `name1=content1; name2=content2;` etc. See [libcurl documentation](http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTCOOKIE) for more information.
476 |
477 | Response arguments:
478 |
479 | * On success, a `torrent-added` object in the form of one of 3.3's torrent objects with the fields for `id`, `name`, and `hashString`.
480 |
481 | * When attempting to add a duplicate torrent, a `torrent-duplicate` object in the same form is returned, but the response's `result` value is still `success`.
482 |
483 | ### 3.5 Removing a torrent
484 | Method name: `torrent-remove`
485 |
486 | | Key | Value Type | Description
487 | |:--|:--|:--
488 | | `ids` | array | torrent list, as described in 3.1
489 | | `delete-local-data` | boolean | delete local data. (default: false)
490 |
491 | Response arguments: none
492 |
493 | ### 3.6 Moving a torrent
494 | Method name: `torrent-set-location`
495 |
496 | Request arguments:
497 |
498 | | Key | Value Type | Description
499 | |:--|:--|:--
500 | | `ids` | array | torrent list, as described in 3.1
501 | | `location` | string | the new torrent location
502 | | `move` | boolean | if true, move from previous location. otherwise, search "location" for files (default: false)
503 |
504 | Response arguments: none
505 |
506 | ### 3.7 Renaming a torrent's path
507 | Method name: `torrent-rename-path`
508 |
509 | For more information on the use of this function, see the transmission.h
510 | documentation of `tr_torrentRenamePath()`. In particular, note that if this
511 | call succeeds you'll want to update the torrent's `files` and `name` field
512 | with `torrent-get`.
513 |
514 | Request arguments:
515 |
516 | | Key | Value Type | Description
517 | |:--|:--|:--
518 | | `ids` | array | the torrent list, as described in 3.1 (must only be 1 torrent)
519 | | `path` | string | the path to the file or folder that will be renamed
520 | | `name` | string | the file or folder's new name
521 |
522 | Response arguments: `path`, `name`, and `id`, holding the torrent ID integer
523 |
524 | ## 4 Session requests
525 | ### 4.1 Session arguments
526 | | Key | Value Type | Description
527 | |:--|:--|:--
528 | | `alt-speed-down` | number | max global download speed (KBps)
529 | | `alt-speed-enabled` | boolean | true means use the alt speeds
530 | | `alt-speed-time-begin` | number | when to turn on alt speeds (units: minutes after midnight)
531 | | `alt-speed-time-day` | number | what day(s) to turn on alt speeds (look at tr_sched_day)
532 | | `alt-speed-time-enabled` | boolean | true means the scheduled on/off times are used
533 | | `alt-speed-time-end` | number | when to turn off alt speeds (units: same)
534 | | `alt-speed-up` | number | max global upload speed (KBps)
535 | | `blocklist-enabled` | boolean | true means enabled
536 | | `blocklist-size` | number | number of rules in the blocklist
537 | | `blocklist-url` | string | location of the blocklist to use for `blocklist-update`
538 | | `cache-size-mb` | number | maximum size of the disk cache (MB)
539 | | `config-dir` | string | location of transmission's configuration directory
540 | | `default-trackers` | string | announce URLs, one per line, and a blank line between [tiers](https://www.bittorrent.org/beps/bep_0012.html).
541 | | `dht-enabled` | boolean | true means allow DHT in public torrents
542 | | `download-dir` | string | default path to download torrents
543 | | `download-dir-free-space` | number | **DEPRECATED** Use the `free-space` method instead.
544 | | `download-queue-enabled` | boolean | if true, limit how many torrents can be downloaded at once
545 | | `download-queue-size` | number | max number of torrents to download at once (see download-queue-enabled)
546 | | `encryption` | string | `required`, `preferred`, `tolerated`
547 | | `idle-seeding-limit-enabled` | boolean | true if the seeding inactivity limit is honored by default
548 | | `idle-seeding-limit` | number | torrents we're seeding will be stopped if they're idle for this long
549 | | `incomplete-dir-enabled` | boolean | true means keep torrents in incomplete-dir until done
550 | | `incomplete-dir` | string | path for incomplete torrents, when enabled
551 | | `lpd-enabled` | boolean | true means allow Local Peer Discovery in public torrents
552 | | `peer-limit-global` | number | maximum global number of peers
553 | | `peer-limit-per-torrent` | number | maximum global number of peers
554 | | `peer-port-random-on-start` | boolean | true means pick a random peer port on launch
555 | | `peer-port` | number | port number
556 | | `pex-enabled` | boolean | true means allow PEX in public torrents
557 | | `port-forwarding-enabled` | boolean | true means ask upstream router to forward the configured peer port to transmission using UPnP or NAT-PMP
558 | | `queue-stalled-enabled` | boolean | whether or not to consider idle torrents as stalled
559 | | `queue-stalled-minutes` | number | torrents that are idle for N minuets aren't counted toward seed-queue-size or download-queue-size
560 | | `rename-partial-files` | boolean | true means append `.part` to incomplete files
561 | | `reqq` | number | the number of outstanding block requests a peer is allowed to queue in the client
562 | | `rpc-version-minimum` | number | the minimum RPC API version supported
563 | | `rpc-version-semver` | string | the current RPC API version in a [semver](https://semver.org)-compatible string
564 | | `rpc-version` | number | the current RPC API version
565 | | `script-torrent-added-enabled` | boolean | whether or not to call the `added` script
566 | | `script-torrent-added-filename` | string | filename of the script to run
567 | | `script-torrent-done-enabled` | boolean | whether or not to call the `done` script
568 | | `script-torrent-done-filename` | string | filename of the script to run
569 | | `script-torrent-done-seeding-enabled` | boolean | whether or not to call the `seeding-done` script
570 | | `script-torrent-done-seeding-filename` | string | filename of the script to run
571 | | `seed-queue-enabled` | boolean | if true, limit how many torrents can be uploaded at once
572 | | `seed-queue-size` | number | max number of torrents to uploaded at once (see seed-queue-enabled)
573 | | `seedRatioLimit` | double | the default seed ratio for torrents to use
574 | | `seedRatioLimited` | boolean | true if seedRatioLimit is honored by default
575 | | `session-id` | string | the current `X-Transmission-Session-Id` value
576 | | `speed-limit-down-enabled` | boolean | true means enabled
577 | | `speed-limit-down` | number | max global download speed (KBps)
578 | | `speed-limit-up-enabled` | boolean | true means enabled
579 | | `speed-limit-up` | number | max global upload speed (KBps)
580 | | `start-added-torrents` | boolean | true means added torrents will be started right away
581 | | `trash-original-torrent-files` | boolean | true means the .torrent file of added torrents will be deleted
582 | | `units` | object | see below
583 | | `utp-enabled` | boolean | true means allow UTP
584 | | `version` | string | long version string `$version ($revision)`
585 |
586 |
587 | `units`: an object containing:
588 |
589 | | Key | Value Type | transmission.h source
590 | |:--|:--|:--
591 | | `speed-units` | array | 4 strings: KB/s, MB/s, GB/s, TB/s
592 | | `speed-bytes` | number | number of bytes in a KB (1000 for kB; 1024 for KiB)
593 | | `size-units` | array | 4 strings: KB/s, MB/s, GB/s, TB/s
594 | | `size-bytes` | number | number of bytes in a KB (1000 for kB; 1024 for KiB)
595 | | `memory-units` | array | 4 strings: KB/s, MB/s, GB/s, TB/s
596 | | `memory-bytes` | number | number of bytes in a KB (1000 for kB; 1024 for KiB)
597 |
598 | `rpc-version` indicates the RPC interface version supported by the RPC server.
599 | It is incremented when a new version of Transmission changes the RPC interface.
600 |
601 | `rpc-version-minimum` indicates the oldest API supported by the RPC server.
602 | It is changes when a new version of Transmission changes the RPC interface
603 | in a way that is not backwards compatible. There are no plans for this
604 | to be common behavior.
605 |
606 | #### 4.1.1 Mutators
607 | Method name: `session-set`
608 |
609 | Request arguments: the mutable properties from 4.1's arguments, i.e. all of them
610 | except:
611 |
612 | * `blocklist-size`
613 | * `config-dir`
614 | * `rpc-version-minimum`,
615 | * `rpc-version-semver`
616 | * `rpc-version`
617 | * `session-id`
618 | * `units`
619 | * `version`
620 |
621 | Response arguments: none
622 |
623 | #### 4.1.2 Accessors
624 | Method name: `session-get`
625 |
626 | Request arguments: an optional `fields` array of keys (see 4.1)
627 |
628 | Response arguments: key/value pairs matching the request's `fields`
629 | argument if present, or all supported fields (see 4.1) otherwise.
630 |
631 | ### 4.2 Session statistics
632 | Method name: `session-stats`
633 |
634 | Request arguments: none
635 |
636 | Response arguments:
637 |
638 | | Key | Value Type | Description
639 | |:--|:--|:--
640 | | `activeTorrentCount` | number
641 | | `downloadSpeed` | number
642 | | `pausedTorrentCount` | number
643 | | `torrentCount` | number
644 | | `uploadSpeed` | number
645 | | `cumulative-stats` | stats object (see below)
646 | | `current-stats` | stats object (see below)
647 |
648 | A stats object contains:
649 |
650 | | Key | Value Type | transmission.h source
651 | |:--|:--|:--
652 | | `uploadedBytes` | number | tr_session_stats
653 | | `downloadedBytes` | number | tr_session_stats
654 | | `filesAdded` | number | tr_session_stats
655 | | `sessionCount` | number | tr_session_stats
656 | | `secondsActive` | number | tr_session_stats
657 |
658 | ### 4.3 Blocklist
659 | Method name: `blocklist-update`
660 |
661 | Request arguments: none
662 |
663 | Response arguments: a number `blocklist-size`
664 |
665 | ### 4.4 Port checking
666 | This method tests to see if your incoming peer port is accessible
667 | from the outside world.
668 |
669 | Method name: `port-test`
670 |
671 | Request arguments: an optional argument `ipProtocol`.
672 | `ipProtocol` is a string specifying the IP protocol version to be used for the port test.
673 | Set to `ipv4` to check IPv4, or set to `ipv6` to check IPv6.
674 | For backwards compatibility, it is allowed to omit this argument to get the behaviour before Transmission `4.1.0`,
675 | which is to check whichever IP protocol the OS happened to use to connect to our port test service,
676 | frankly not very useful.
677 |
678 | Response arguments:
679 |
680 | | Key | Value Type | Description
681 | | :-- | :-- | :--
682 | | `port-is-open` | boolean | true if port is open, false if port is closed
683 | | `ipProtocol` | string | `ipv4` if the test was carried out on IPv4, `ipv6` if the test was carried out on IPv6, unset if it cannot be determined
684 |
685 | ### 4.5 Session shutdown
686 | This method tells the transmission session to shut down.
687 |
688 | Method name: `session-close`
689 |
690 | Request arguments: none
691 |
692 | Response arguments: none
693 |
694 | ### 4.6 Queue movement requests
695 | | Method name | transmission.h source
696 | |:--|:--
697 | | `queue-move-top` | tr_torrentQueueMoveTop()
698 | | `queue-move-up` | tr_torrentQueueMoveUp()
699 | | `queue-move-down` | tr_torrentQueueMoveDown()
700 | | `queue-move-bottom` | tr_torrentQueueMoveBottom()
701 |
702 | Request arguments:
703 |
704 | | Key | Value Type | Description
705 | |:--|:--|:--
706 | | `ids` | array | torrent list, as described in 3.1.
707 |
708 | Response arguments: none
709 |
710 | ### 4.7 Free space
711 | This method tests how much free space is available in a
712 | client-specified folder.
713 |
714 | Method name: `free-space`
715 |
716 | Request arguments:
717 |
718 | | Key | Value type | Description
719 | |:--|:--|:--
720 | | `path` | string | the directory to query
721 |
722 | Response arguments:
723 |
724 | | Key | Value type | Description
725 | |:--|:--|:--
726 | | `path` | string | same as the Request argument
727 | | `size-bytes` | number | the size, in bytes, of the free space in that directory
728 | | `total_size` | number | the total capacity, in bytes, of that directory
729 |
730 | ### 4.8 Bandwidth groups
731 | #### 4.8.1 Bandwidth group mutator: `group-set`
732 | Method name: `group-set`
733 |
734 | Request parameters:
735 |
736 | | Key | Value type | Description
737 | |:--|:--|:--
738 | | `honorsSessionLimits` | boolean | true if session upload limits are honored
739 | | `name` | string | Bandwidth group name
740 | | `speed-limit-down-enabled` | boolean | true means enabled
741 | | `speed-limit-down` | number | max global download speed (KBps)
742 | | `speed-limit-up-enabled` | boolean | true means enabled
743 | | `speed-limit-up` | number | max global upload speed (KBps)
744 |
745 | Response arguments: none
746 |
747 | #### 4.8.2 Bandwidth group accessor: `group-get`
748 | Method name: `group-get`
749 |
750 | Request arguments: An optional argument `group`.
751 | `group` is either a string naming the bandwidth group,
752 | or a list of such strings.
753 | If `group` is omitted, all bandwidth groups are used.
754 |
755 | Response arguments:
756 |
757 | | Key | Value type | Description
758 | |:--|:--|:--
759 | |`group`| array | A list of bandwidth group description objects
760 |
761 | A bandwidth group description object has:
762 |
763 | | Key | Value type | Description
764 | |:--|:--|:--
765 | | `honorsSessionLimits` | boolean | true if session upload limits are honored
766 | | `name` | string | Bandwidth group name
767 | | `speed-limit-down-enabled` | boolean | true means enabled
768 | | `speed-limit-down` | number | max global download speed (KBps)
769 | | `speed-limit-up-enabled` | boolean | true means enabled
770 | | `speed-limit-up` | number | max global upload speed (KBps)
771 |
772 | ## 5 Protocol versions
773 | This section lists the changes that have been made to the RPC protocol.
774 |
775 | There are two ways to check for API compatibility. Since most developers know
776 | [semver](https://semver.org/), session-get's `rpc-version-semver` is the
777 | recommended way. That value is a semver-compatible string of the RPC protocol
778 | version number.
779 |
780 | Since Transmission predates the semver 1.0 spec, the previous scheme was for
781 | the RPC version to be a whole number and to increment it whenever a change was
782 | made. That is session-get's `rpc-version`. `rpc-version-minimum` lists the
783 | oldest version that is compatible with the current version; i.e. an app coded
784 | to use `rpc-version-minimum` would still work on a Transmission release running
785 | `rpc-version`.
786 |
787 | Breaking changes are denoted with a :bomb: emoji.
788 |
789 | Transmission 1.30 (`rpc-version-semver` 1.0.0, `rpc-version`: 1)
790 |
791 | Initial revision.
792 |
793 | Transmission 1.40 (`rpc-version-semver` 1.1.0, `rpc-version`: 2)
794 |
795 | | Method | Description
796 | |:---|:---
797 | | `torrent-get` | new `port` to `peers`
798 |
799 | Transmission 1.41 (`rpc-version-semver` 1.2.0, `rpc-version`: 3)
800 |
801 | | Method | Description
802 | |:---|:---
803 | | `session-get` | new arg `version`
804 | | `torrent-get` | new arg `downloaders`
805 | | `torrent-remove` | new method
806 |
807 | Transmission 1.50 (`rpc-version-semver` 1.3.0, `rpc-version`: 4)
808 |
809 | | Method | Description
810 | |:---|:---
811 | |`session-get` | new arg `rpc-version-minimum`
812 | |`session-get` | new arg `rpc-version`
813 | |`session-stats` | added `cumulative-stats`
814 | |`session-stats` | added `current-stats`
815 | |`torrent-get` | new arg `downloadDir`
816 |
817 | Transmission 1.60 (`rpc-version-semver` 2.0.0, `rpc-version`: 5)
818 |
819 | | Method | Description
820 | |:---|:---
821 | | `session-get` | :bomb: renamed `peer-limit` to `peer-limit-global`
822 | | `session-get` | :bomb: renamed `pex-allowed` to `pex-enabled`
823 | | `session-get` | :bomb: renamed `port` to `peer-port`
824 | | `torrent-get` | :bomb: removed arg `downloadLimitMode`
825 | | `torrent-get` | :bomb: removed arg `uploadLimitMode`
826 | | `torrent-set` | :bomb: renamed `speed-limit-down-enabled` to `downloadLimited`
827 | | `torrent-set` | :bomb: renamed `speed-limit-down` to `downloadLimit`
828 | | `torrent-set` | :bomb: renamed `speed-limit-up-enabled` to `uploadLimited`
829 | | `torrent-set` | :bomb: renamed `speed-limit-up` to `uploadLimit`
830 | | `blocklist-update` | new method
831 | | `port-test` | new method
832 | | `session-get` | new arg `alt-speed-begin`
833 | | `session-get` | new arg `alt-speed-down`
834 | | `session-get` | new arg `alt-speed-enabled`
835 | | `session-get` | new arg `alt-speed-end`
836 | | `session-get` | new arg `alt-speed-time-enabled`
837 | | `session-get` | new arg `alt-speed-up`
838 | | `session-get` | new arg `blocklist-enabled`
839 | | `session-get` | new arg `blocklist-size`
840 | | `session-get` | new arg `peer-limit-per-torrent`
841 | | `session-get` | new arg `seedRatioLimit`
842 | | `session-get` | new arg `seedRatioLimited`
843 | | `torrent-add` | new arg `files-unwanted`
844 | | `torrent-add` | new arg `files-wanted`
845 | | `torrent-add` | new arg `priority-high`
846 | | `torrent-add` | new arg `priority-low`
847 | | `torrent-add` | new arg `priority-normal`
848 | | `torrent-get` | new arg `bandwidthPriority`
849 | | `torrent-get` | new arg `fileStats`
850 | | `torrent-get` | new arg `honorsSessionLimits`
851 | | `torrent-get` | new arg `percentDone`
852 | | `torrent-get` | new arg `pieces`
853 | | `torrent-get` | new arg `seedRatioLimit`
854 | | `torrent-get` | new arg `seedRatioMode`
855 | | `torrent-get` | new arg `torrentFile`
856 | | `torrent-get` | new ids option `recently-active`
857 | | `torrent-reannounce` | new method
858 | | `torrent-set` | new arg `bandwidthPriority`
859 | | `torrent-set` | new arg `honorsSessionLimits`
860 | | `torrent-set` | new arg `seedRatioLimit`
861 | | `torrent-set` | new arg `seedRatioLimited`
862 |
863 | Transmission 1.70 (`rpc-version-semver` 2.1.0, `rpc-version`: 6)
864 |
865 | | Method | Description
866 | |:---|:---
867 | | `method torrent-set-location` | new method
868 |
869 | Transmission 1.80 (`rpc-version-semver` 3.0.0, `rpc-version`: 7)
870 |
871 | | Method | Description
872 | |:---|:---
873 | | `torrent-get` | :bomb: removed arg `announceResponse` (use `trackerStats instead`)
874 | | `torrent-get` | :bomb: removed arg `announceURL` (use `trackerStats instead`)
875 | | `torrent-get` | :bomb: removed arg `downloaders` (use `trackerStats instead`)
876 | | `torrent-get` | :bomb: removed arg `lastAnnounceTime` (use `trackerStats instead`)
877 | | `torrent-get` | :bomb: removed arg `lastScrapeTime` (use `trackerStats instead`)
878 | | `torrent-get` | :bomb: removed arg `leechers` (use `trackerStats instead`)
879 | | `torrent-get` | :bomb: removed arg `nextAnnounceTime` (use `trackerStats instead`)
880 | | `torrent-get` | :bomb: removed arg `nextScrapeTime` (use `trackerStats instead`)
881 | | `torrent-get` | :bomb: removed arg `scrapeResponse` (use `trackerStats instead`)
882 | | `torrent-get` | :bomb: removed arg `scrapeURL` (use `trackerStats instead`)
883 | | `torrent-get` | :bomb: removed arg `seeders` (use `trackerStats instead`)
884 | | `torrent-get` | :bomb: removed arg `swarmSpeed`
885 | | `torrent-get` | :bomb: removed arg `timesCompleted` (use `trackerStats instead`)
886 | | `session-set` | new arg `incomplete-dir-enabled`
887 | | `session-set` | new arg `incomplete-dir`
888 | | `torrent-get` | new arg `magnetLink`
889 | | `torrent-get` | new arg `metadataPercentComplete`
890 | | `torrent-get` | new arg `trackerStats`
891 |
892 | Transmission 1.90 (`rpc-version-semver` 3.1.0, `rpc-version`: 8)
893 |
894 | | Method | Description
895 | |:---|:---
896 | | `session-set` | new arg `rename-partial-files`
897 | | `session-get` | new arg `rename-partial-files`
898 | | `session-get` | new arg `config-dir`
899 | | `torrent-add` | new arg `bandwidthPriority`
900 | | `torrent-get` | new trackerStats arg `lastAnnounceTimedOut`
901 |
902 | Transmission 1.92 (`rpc-version-semver` 3.2.0, `rpc-version`: 8)
903 |
904 | Note: `rpc-version` was not bumped in this release due to an oversight.
905 |
906 | | Method | Description
907 | |:---|:---
908 | | `torrent-get` | new trackerStats arg `lastScrapeTimedOut`
909 |
910 | Transmission 2.00 (`rpc-version-semver` 3.3.0, `rpc-version`: 9)
911 |
912 | | Method | Description
913 | |:---|:---
914 | | `session-set` | new arg `start-added-torrents`
915 | | `session-set` | new arg `trash-original-torrent-files`
916 | | `session-get` | new arg `start-added-torrents`
917 | | `session-get` | new arg `trash-original-torrent-files`
918 | | `torrent-get` | new arg `isFinished`
919 |
920 | Transmission 2.10 (`rpc-version-semver` 3.4.0, `rpc-version`: 10)
921 |
922 | | Method | Description
923 | |:---|:---
924 | | `session-get` | new arg `cache-size-mb`
925 | | `session-get` | new arg `units`
926 | | `session-set` | new arg `idle-seeding-limit-enabled`
927 | | `session-set` | new arg `idle-seeding-limit`
928 | | `torrent-set` | new arg `seedIdleLimit`
929 | | `torrent-set` | new arg `seedIdleMode`
930 | | `torrent-set` | new arg `trackerAdd`
931 | | `torrent-set` | new arg `trackerRemove`
932 | | `torrent-set` | new arg `trackerReplace`
933 |
934 | Transmission 2.12 (`rpc-version-semver` 3.5.0, `rpc-version`: 11)
935 |
936 | | Method | Description
937 | |:---|:---
938 | | `session-get` | new arg `blocklist-url`
939 | | `session-set` | new arg `blocklist-url`
940 |
941 | Transmission 2.20 (`rpc-version-semver` 3.6.0, `rpc-version`: 12)
942 |
943 | | Method | Description
944 | |:---|:---
945 | | `session-get` | new arg `download-dir-free-space`
946 | | `session-close` | new method
947 |
948 | Transmission 2.30 (`rpc-version-semver` 4.0.0, `rpc-version`: 13)
949 |
950 | | Method | Description
951 | |:---|:---
952 | | `torrent-get` | :bomb: removed arg `peersKnown`
953 | | `session-get` | new arg `isUTP` to the `peers` list
954 | | `torrent-add` | new arg `cookies`
955 |
956 | Transmission 2.40 (`rpc-version-semver` 5.0.0, `rpc-version`: 14)
957 |
958 | | Method | Description
959 | |:---|:---
960 | | `torrent-get` | :bomb: values of `status` field changed
961 | | `queue-move-bottom` | new method
962 | | `queue-move-down` | new method
963 | | `queue-move-top` | new method
964 | | `session-set` | new arg `download-queue-enabled`
965 | | `session-set` | new arg `download-queue-size`
966 | | `session-set` | new arg `queue-stalled-enabled`
967 | | `session-set` | new arg `queue-stalled-minutes`
968 | | `session-set` | new arg `seed-queue-enabled`
969 | | `session-set` | new arg `seed-queue-size`
970 | | `torrent-get` | new arg `fromLpd` in peersFrom
971 | | `torrent-get` | new arg `isStalled`
972 | | `torrent-get` | new arg `queuePosition`
973 | | `torrent-set` | new arg `queuePosition`
974 | | `torrent-start-now` | new method
975 |
976 | Transmission 2.80 (`rpc-version-semver` 5.1.0, `rpc-version`: 15)
977 |
978 | | Method | Description
979 | |:---|:---
980 | | `torrent-get` | new arg `etaIdle`
981 | | `torrent-rename-path` | new method
982 | | `free-space` | new method
983 | | `torrent-add` | new return arg `torrent-duplicate`
984 |
985 | Transmission 3.00 (`rpc-version-semver` 5.2.0, `rpc-version`: 16)
986 |
987 | | Method | Description
988 | |:---|:---
989 | | `session-get` | new request arg `fields`
990 | | `session-get` | new arg `session-id`
991 | | `torrent-get` | new arg `labels`
992 | | `torrent-set` | new arg `labels`
993 | | `torrent-get` | new arg `editDate`
994 | | `torrent-get` | new request arg `format`
995 |
996 | Transmission 4.0.0 (`rpc-version-semver` 5.3.0, `rpc-version`: 17)
997 |
998 | | Method | Description
999 | |:---|:---
1000 | | `/upload` | :warning: undocumented `/upload` endpoint removed
1001 | | `session-get` | :warning: **DEPRECATED** `download-dir-free-space`. Use `free-space` instead.
1002 | | `free-space` | new return arg `total_size`
1003 | | `session-get` | new arg `default-trackers`
1004 | | `session-get` | new arg `rpc-version-semver`
1005 | | `session-get` | new arg `script-torrent-added-enabled`
1006 | | `session-get` | new arg `script-torrent-added-filename`
1007 | | `session-get` | new arg `script-torrent-done-seeding-enabled`
1008 | | `session-get` | new arg `script-torrent-done-seeding-filename`
1009 | | `torrent-add` | new arg `labels`
1010 | | `torrent-get` | new arg `availability`
1011 | | `torrent-get` | new arg `file-count`
1012 | | `torrent-get` | new arg `group`
1013 | | `torrent-get` | new arg `percentComplete`
1014 | | `torrent-get` | new arg `primary-mime-type`
1015 | | `torrent-get` | new arg `tracker.sitename`
1016 | | `torrent-get` | new arg `trackerStats.sitename`
1017 | | `torrent-get` | new arg `trackerList`
1018 | | `torrent-set` | new arg `group`
1019 | | `torrent-set` | new arg `trackerList`
1020 | | `torrent-set` | :warning: **DEPRECATED** `trackerAdd`. Use `trackerList` instead.
1021 | | `torrent-set` | :warning: **DEPRECATED** `trackerRemove`. Use `trackerList` instead.
1022 | | `torrent-set` | :warning: **DEPRECATED** `trackerReplace`. Use `trackerList` instead.
1023 | | `group-set` | new method
1024 | | `group-get` | new method
1025 | | `torrent-get` | :warning: old arg `wanted` was implemented as an array of `0` or `1` in Transmission 3.00 and older, despite being documented as an array of booleans. Transmission 4.0.0 and 4.0.1 "fixed" this by returning an array of booleans; but in practical terms, this change caused an unannounced breaking change for any 3rd party code that expected `0` or `1`. For this reason, 4.0.2 restored the 3.00 behavior and updated this spec to match the code.
1026 |
1027 | Transmission 4.1.0 (`rpc-version-semver` 5.4.0, `rpc-version`: 18)
1028 | | Method | Description
1029 | |:---|:---
1030 | | `torrent-get` | new arg `sequentialDownload`
1031 | | `torrent-set` | new arg `sequentialDownload`
1032 | | `torrent-get` | new arg `files.beginPiece`
1033 | | `torrent-get` | new arg `files.endPiece`
1034 | | `port-test` | new arg `ipProtocol`
--------------------------------------------------------------------------------
/scripts/attach-shell.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | docker exec -it $1 bash
4 |
--------------------------------------------------------------------------------
/scripts/build-images.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | docker image build -t mhadam/transmission -f ../docker/transmission.df ..
4 | docker image build -t mhadam/clutch -f ../docker/clutch.df ..
5 |
--------------------------------------------------------------------------------
/scripts/run-containers.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | docker-compose -f ../docker/docker-compose.yml up -d
4 |
--------------------------------------------------------------------------------
/scripts/run-transmission.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | docker run --rm mhadam/transmission
4 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/__init__.py
--------------------------------------------------------------------------------
/tests/endtoend/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/endtoend/__init__.py
--------------------------------------------------------------------------------
/tests/endtoend/test_accessor.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | # Enabling debugging at http.client level (requests->urllib3->http.client)
4 | # you will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA.
5 | # the only thing missing will be the response.body which is not logged.
6 | from http.client import HTTPConnection
7 |
8 | from clutch.client import Client
9 | from clutch.network.rpc.message import Response
10 |
11 | HTTPConnection.debuglevel = 1 # type: ignore
12 |
13 | logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from requests
14 | logging.getLogger().setLevel(logging.DEBUG)
15 | requests_log = logging.getLogger("urllib3")
16 | requests_log.setLevel(logging.DEBUG)
17 | requests_log.propagate = True
18 |
19 |
20 | def test_retrieve_all_fields():
21 | tag = 16
22 | client = Client(host="transmission")
23 |
24 | response: Response = client.torrent.accessor(all_fields=True, tag=tag)
25 |
26 | assert response.result == "success"
27 | assert response.tag == tag
28 | assert "little_women" in {
29 | x["name"]
30 | for x in response.model_dump(exclude_none=True)["arguments"]["torrents"]
31 | }
32 | assert "ion" in {
33 | x["name"]
34 | for x in response.model_dump(exclude_none=True)["arguments"]["torrents"]
35 | }
36 |
37 |
38 | def test_retrieve_two_fields():
39 | tag = 16
40 | client = Client(host="transmission")
41 |
42 | response: Response = client.torrent.accessor(fields=["id", "name"], tag=tag)
43 |
44 | assert response.result == "success"
45 | assert response.tag == tag
46 | assert "id" in response.model_dump(exclude_none=True)["arguments"]["torrents"][0]
47 | assert "name" in response.model_dump(exclude_none=True)["arguments"]["torrents"][0]
48 |
--------------------------------------------------------------------------------
/tests/endtoend/test_action.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/endtoend/test_action.py
--------------------------------------------------------------------------------
/tests/endtoend/test_mutator.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | # Enabling debugging at http.client level (requests->urllib3->http.client)
4 | # you will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA.
5 | # the only thing missing will be the response.body which is not logged.
6 | from http.client import HTTPConnection
7 |
8 | HTTPConnection.debuglevel = 1 # type: ignore
9 |
10 | logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from requests
11 | logging.getLogger().setLevel(logging.DEBUG)
12 | requests_log = logging.getLogger("urllib3")
13 | requests_log.setLevel(logging.DEBUG)
14 | requests_log.propagate = True
15 |
16 |
17 | def test_server_returns_same_tag():
18 | pass
19 | # tag = 15
20 | # mutator_args: TorrentMutatorArguments = {}
21 | # client = Client(host="transmission")
22 | #
23 | # response: Response = client.torrent.mutator(mutator_args, tag)
24 | #
25 | # assert response["tag"] == tag
26 |
27 |
28 | def test_conversion_torrent_replace():
29 | pass
30 |
--------------------------------------------------------------------------------
/tests/mock/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/mock/__init__.py
--------------------------------------------------------------------------------
/tests/mock/conftest.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | from pathlib import Path
4 | from typing import Any, Callable
5 |
6 | import pytest
7 | import pytest_httpserver
8 | from deepdiff import DeepDiff
9 | from dirty_equals import FunctionCheck
10 |
11 | from clutch.client import Client
12 |
13 |
14 | @pytest.fixture(scope="function")
15 | def client(httpserver):
16 | client = Client(port=httpserver.port)
17 | return client
18 |
19 |
20 | def custom_compare(expect) -> Callable[[Any], bool]:
21 | def inner(actual):
22 | diff = DeepDiff(actual, expect, ignore_order=True)
23 | is_same = len(diff) == 0
24 | if not is_same:
25 | msg = json.dumps(
26 | {
27 | "expect": expect,
28 | "actual": actual,
29 | "diff": json.loads(diff.to_json()),
30 | },
31 | indent=2,
32 | )
33 | logging.getLogger().warn(msg=msg)
34 | return is_same
35 |
36 | return inner
37 |
38 |
39 | @pytest.fixture(scope="function")
40 | def mocker(monkeypatch, httpserver):
41 | def set_mock(req=None, res=None):
42 | r = {k: v for k, v in res.items() if v is not None}
43 | if req is None:
44 | httpserver.expect_request(
45 | "/transmission/rpc", method="POST"
46 | ).respond_with_json(r)
47 | else:
48 | httpserver.expect_request(
49 | "/transmission/rpc",
50 | method="POST",
51 | json=FunctionCheck(custom_compare(req)),
52 | ).respond_with_json(r)
53 |
54 | return set_mock
55 |
--------------------------------------------------------------------------------
/tests/mock/test_bandwidth_groups.py:
--------------------------------------------------------------------------------
1 | def test_bandwidth_groups(httpserver, mocker, client):
2 | mocker(
3 | req={"method": "group-get", "arguments": {"group": "abc"}},
4 | res={
5 | "result": "success",
6 | "arguments": {
7 | "group": [
8 | {
9 | "honorsSessionLimits": True,
10 | "name": "",
11 | "speed-limit-down-enabled": True,
12 | "speed-limit-down": 1,
13 | "speed-limit-up-enabled": True,
14 | "speed-limit-up": 1,
15 | }
16 | ]
17 | },
18 | },
19 | )
20 | _ = client.misc.bandwidth_groups(group="abc")
21 | httpserver.check_assertions()
22 |
--------------------------------------------------------------------------------
/tests/mock/test_cases.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Callable, Any
3 |
4 | from clutch.schema.user.method.torrent.action import TorrentActionMethod
5 |
6 | from clutch import Client
7 |
8 | test_cases: list[tuple[str, dict, dict, Callable[[Client], Any]]] = [
9 | (
10 | "torrent add",
11 | {
12 | "method": "torrent-add",
13 | "arguments": {"filename": "example.torrent"},
14 | "tag": 1,
15 | },
16 | {
17 | "result": "success",
18 | "arguments": {
19 | "torrent-added": {"id": 1, "name": "example", "hashString": "abc"}
20 | },
21 | },
22 | lambda c: c.torrent.add({"filename": "example.torrent"}, tag=1),
23 | ),
24 | (
25 | "torrent add with magnet link",
26 | {
27 | "method": "torrent-add",
28 | "arguments": {"filename": "magnet:?xt=urn:btih:examplehash"},
29 | },
30 | {
31 | "result": "success",
32 | "arguments": {
33 | "torrent-added": {
34 | "id": 2,
35 | "name": "magnet-example",
36 | "hashString": "abc",
37 | }
38 | },
39 | },
40 | lambda c: c.torrent.add({"filename": "magnet:?xt=urn:btih:examplehash"}),
41 | ),
42 | (
43 | "torrent get with objects format",
44 | {
45 | "method": "torrent-get",
46 | "arguments": {"format": "objects", "fields": ["status", "name", "id"]},
47 | "tag": 2,
48 | },
49 | {
50 | "result": "success",
51 | "arguments": {
52 | "torrents": [
53 | {"id": 1, "name": "example", "status": "downloading"},
54 | {"id": 2, "name": "magnet-example", "status": "stopped"},
55 | ]
56 | },
57 | },
58 | lambda c: c.torrent.accessor(fields={"id", "name", "status"}, tag=2),
59 | ),
60 | (
61 | "torrent set location",
62 | {
63 | "method": "torrent-set-location",
64 | "arguments": {"ids": [1], "location": "/new/path", "move": True},
65 | "tag": 3,
66 | },
67 | {"result": "success"},
68 | lambda c: c.torrent.move([1], "/new/path", move=True, tag=3),
69 | ),
70 | (
71 | "torrent action - stop",
72 | {"method": "torrent-stop", "arguments": {"ids": [1]}},
73 | {"result": "success"},
74 | lambda c: c.torrent.action(TorrentActionMethod.STOP, ids=[1]),
75 | ),
76 | (
77 | "torrent start",
78 | {"method": "torrent-start", "arguments": {"ids": [2]}},
79 | {"result": "success"},
80 | lambda c: c.torrent.action(TorrentActionMethod.START, ids=[2]),
81 | ),
82 | (
83 | "torrent remove",
84 | {
85 | "method": "torrent-remove",
86 | "arguments": {"ids": [1], "delete-local-data": True},
87 | "tag": 4,
88 | },
89 | {"result": "success"},
90 | lambda c: c.torrent.remove(tag=4, delete_local_data=True, ids=[1]),
91 | ),
92 | (
93 | "session stats",
94 | {"method": "session-stats", "tag": 1},
95 | {
96 | "result": "success",
97 | "arguments": {
98 | "activeTorrentCount": 1,
99 | "downloadSpeed": 1024,
100 | "uploadSpeed": 512,
101 | },
102 | },
103 | lambda c: c.session.stats(tag=1),
104 | ),
105 | (
106 | "session set",
107 | {
108 | "method": "session-set",
109 | "arguments": {"download-dir": "/new/download/path"},
110 | "tag": 5,
111 | },
112 | {"result": "success"},
113 | lambda c: c.session.mutator(
114 | arguments={"download_dir": "/new/download/path"}, tag=5
115 | ),
116 | ),
117 | (
118 | "session get",
119 | {
120 | "method": "session-get",
121 | "arguments": {"fields": ["version", "rpc-version", "download-dir"]},
122 | },
123 | {
124 | "result": "success",
125 | "arguments": {
126 | "version": "3.00",
127 | "rpc-version": 17,
128 | "download-dir": "/new/download/path",
129 | },
130 | },
131 | lambda c: c.session.accessor(fields={"version", "rpc_version", "download_dir"}),
132 | ),
133 | (
134 | "free space",
135 | {"method": "free-space", "arguments": {"path": "/test/dir"}, "tag": 5},
136 | {
137 | "result": "success",
138 | "arguments": {
139 | "path": "/test/dir",
140 | "size-bytes": 1024 * 1024 * 1024,
141 | },
142 | },
143 | lambda c: c.misc.free_space(path="/test/dir", tag=5),
144 | ),
145 | ]
146 |
147 |
148 | def test_all(client, httpserver, mocker):
149 | for i, case in enumerate(test_cases):
150 | desc, req, res, call = case
151 | logging.getLogger().info(f"running {desc}")
152 | mocker(req=req, res=res)
153 | try:
154 | call(client)
155 | except Exception as e:
156 | logging.getLogger().info(e)
157 | httpserver.check_assertions()
158 | httpserver.clear()
159 |
--------------------------------------------------------------------------------
/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/unit/__init__.py
--------------------------------------------------------------------------------
/tests/unit/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/unit/client/__init__.py
--------------------------------------------------------------------------------
/tests/unit/client/conftest.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import create_autospec
2 |
3 | import pytest
4 |
5 | from clutch.client import Client
6 | from clutch.network.connection import Connection
7 | from clutch.network.session import TransmissionSession
8 |
9 |
10 | @pytest.fixture(scope="function")
11 | def client():
12 | session = create_autospec(TransmissionSession)
13 | connection = Connection("127.0.0.1", session)
14 | client = Client()
15 | client._connection = connection
16 | return client
17 |
--------------------------------------------------------------------------------
/tests/unit/client/session/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/unit/client/session/__init__.py
--------------------------------------------------------------------------------
/tests/unit/client/session/test_accessor.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 | from dirty_equals import IsList
4 |
5 |
6 | def test_session_accessor(client):
7 | tag = 5
8 | try:
9 | client.session.accessor(fields={"rpc_version", "seed_ratio_limit"}, tag=tag)
10 | except Exception as e:
11 | print(e)
12 |
13 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
14 | assert request_data["method"] == "session-get"
15 | assert request_data["arguments"] == {
16 | "fields": IsList("rpc-version", "seedRatioLimit", check_order=False)
17 | }
18 | assert request_data["tag"] == tag
19 |
--------------------------------------------------------------------------------
/tests/unit/client/session/test_mutator.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 |
4 | def test_session_mutator_arguments(client):
5 | try:
6 | client.session.mutator(
7 | arguments={"seed_ratio_limited": True, "seed_queue_size": 10}
8 | )
9 | except Exception as e:
10 | pass
11 |
12 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
13 | assert request_data["method"] == "session-set"
14 | assert request_data["arguments"] == {
15 | "seed-queue-size": 10,
16 | "seedRatioLimited": True,
17 | }
18 |
--------------------------------------------------------------------------------
/tests/unit/client/test_queue.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 | from clutch.method.queue import QueueMovement
4 |
5 |
6 | def test_queue_move(client):
7 | try:
8 | client.queue.move(movement=QueueMovement.TOP, ids=[1, 2])
9 | except Exception as e:
10 | pass
11 |
12 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
13 | assert request_data["method"] == QueueMovement.TOP.value
14 | assert request_data["arguments"] == {"ids": [1, 2]}
15 |
--------------------------------------------------------------------------------
/tests/unit/client/torrent/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/unit/client/torrent/__init__.py
--------------------------------------------------------------------------------
/tests/unit/client/torrent/test_accessor.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 |
4 | def test_torrent_accessor_fields(client):
5 | try:
6 | client.torrent.accessor(fields=["peer_limit", "tracker_stats"])
7 | except:
8 | pass
9 |
10 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
11 | assert request_data["method"] == "torrent-get"
12 | assert set(request_data["arguments"]["fields"]) == {"peer-limit", "trackerStats"}
13 | assert request_data["arguments"]["format"] == "objects"
14 |
--------------------------------------------------------------------------------
/tests/unit/client/torrent/test_action.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 | from clutch.schema.user.method.torrent.action import TorrentActionMethod
4 |
5 |
6 | def test_torrent_action(client):
7 | try:
8 | client.torrent.action(TorrentActionMethod.START_NOW, ids=[1, 2])
9 | except Exception:
10 | pass
11 |
12 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
13 | assert request_data["method"] == TorrentActionMethod.START_NOW.value
14 | assert request_data["arguments"] == {"ids": [1, 2]}
15 |
--------------------------------------------------------------------------------
/tests/unit/client/torrent/test_add.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 |
4 | def test_torrent_add_filename(client):
5 | file = "some_filename"
6 | try:
7 | client.torrent.add({"filename": file, "priority_low": []})
8 | except Exception as e:
9 | pass
10 |
11 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
12 | assert request_data["method"] == "torrent-add"
13 | assert request_data["arguments"] == {"filename": file, "priority-low": []}
14 |
15 |
16 | def test_torrent_add_metainfo(client):
17 | metainfo = "some_metainfo"
18 | try:
19 | client.torrent.add({"metainfo": metainfo})
20 | except Exception:
21 | pass
22 |
23 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
24 | assert request_data["method"] == "torrent-add"
25 | assert request_data["arguments"] == {"metainfo": metainfo}
26 |
--------------------------------------------------------------------------------
/tests/unit/client/torrent/test_move.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 |
4 | def test_torrent_move(client):
5 | location = "some_location"
6 | try:
7 | client.torrent.move(ids=[1, 2], location=location, move=True)
8 | except Exception as e:
9 | pass
10 |
11 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
12 | assert request_data["method"] == "torrent-set-location"
13 | assert request_data["arguments"] == {
14 | "ids": [1, 2],
15 | "location": location,
16 | "move": True,
17 | }
18 |
--------------------------------------------------------------------------------
/tests/unit/client/torrent/test_mutator.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 |
4 | def test_torrent_mutator(client):
5 | try:
6 | client.torrent.mutator()
7 | except Exception:
8 | pass
9 |
10 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
11 | assert request_data["method"] == "torrent-set"
12 | assert request_data["arguments"] == {}
13 |
14 |
15 | def test_torrent_mutator_arguments(client):
16 | try:
17 | client.torrent.mutator(
18 | ids=["hi", 2],
19 | arguments={"priority_high": [], "download_limited": True, "ids": [3, 4]},
20 | )
21 | except Exception as e:
22 | pass
23 |
24 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
25 | assert request_data["method"] == "torrent-set"
26 | # turn ids into a set since pydantic seems to serialize as a set
27 | # and original ids order differs from assertion
28 | assert len(set(request_data["arguments"]["ids"])) == len(
29 | request_data["arguments"]["ids"]
30 | )
31 | request_data["arguments"]["ids"] = set(request_data["arguments"]["ids"])
32 | assert request_data["arguments"] == {
33 | "ids": {"hi", 2},
34 | "downloadLimited": True,
35 | "priority-high": [],
36 | }
37 |
--------------------------------------------------------------------------------
/tests/unit/client/torrent/test_remove.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 |
4 | def test_torrent_remove(client):
5 | try:
6 | client.torrent.remove(ids=[1, 2], delete_local_data=True)
7 | except Exception as e:
8 | pass
9 |
10 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
11 | assert request_data["method"] == "torrent-remove"
12 | assert request_data["arguments"] == {"ids": [1, 2], "delete-local-data": True}
13 |
--------------------------------------------------------------------------------
/tests/unit/client/torrent/test_rename.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 |
4 | def test_torrent_rename(client):
5 | path = "some_path"
6 | name = "some_name"
7 | try:
8 | client.torrent.rename(ids=[1, 2], path=path, name=name)
9 | except Exception as e:
10 | pass
11 |
12 | request_data = loads(client._connection.session.post.call_args.kwargs["data"])
13 | assert request_data["method"] == "torrent-rename-path"
14 | assert request_data["arguments"] == {"ids": [1, 2], "name": name, "path": path}
15 |
--------------------------------------------------------------------------------
/tests/unit/schema/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/unit/schema/__init__.py
--------------------------------------------------------------------------------
/tests/unit/schema/user/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/unit/schema/user/__init__.py
--------------------------------------------------------------------------------
/tests/unit/schema/user/response/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/unit/schema/user/response/__init__.py
--------------------------------------------------------------------------------
/tests/unit/schema/user/response/torrent/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhadam/clutch/c4d30187f33ba8b1ebb87b573511bc0b400ee8ca/tests/unit/schema/user/response/torrent/__init__.py
--------------------------------------------------------------------------------
/tests/unit/schema/user/response/torrent/test_accessor.py:
--------------------------------------------------------------------------------
1 | from clutch.schema.user.response.torrent.accessor import TorrentAccessorObject
2 |
3 |
4 | def test_optional_fields():
5 | data = {"error": "some string"}
6 |
7 | result = TorrentAccessorObject.construct(**data)
8 |
9 | # as of writing now, construct creates a model without any of the missing fields
10 | assert result.error == "some string"
11 |
--------------------------------------------------------------------------------
/tests/unit/test_networking.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from unittest.mock import create_autospec
3 |
4 | from clutch.network.connection import Connection
5 | from clutch.network.rpc.message import Request, Response
6 | from clutch.network.session import TransmissionSession
7 | from clutch.schema.user.response.torrent.accessor import TorrentAccessorResponse
8 |
9 |
10 | def test_connection_marshals_requests_and_responses():
11 | session = create_autospec(TransmissionSession)
12 | session.post.return_value.text = (
13 | '{"result":"success","arguments":{"test":"response"},"tag":5}'
14 | )
15 | connection = Connection("127.0.0.1", session)
16 | request = Request(method="torrent-get", arguments={"test": "request"}, tag=5)
17 |
18 | response: Optional[Response] = connection.send(request)
19 |
20 | assert response is not None
21 | assert response.arguments["test"] == "response"
22 | assert response.tag == 5
23 | assert response.result == "success"
24 |
25 |
26 | def test_connection_marshals_types():
27 | session = create_autospec(TransmissionSession)
28 | session.post.return_value.text = (
29 | '{"result":"success","arguments":{"torrents":[{"id":1},{"id":2}]},"tag":5}'
30 | )
31 | connection = Connection("127.0.0.1", session)
32 | request = Request(method="torrent-get", arguments={"fields": ["id"]}, tag=5)
33 |
34 | response = connection.send(request, TorrentAccessorResponse)
35 |
36 | assert response is not None
37 | assert isinstance(response.arguments, TorrentAccessorResponse)
38 | assert response.arguments.model_dump(exclude_none=True)["torrents"] == [
39 | {"id": 1},
40 | {"id": 2},
41 | ]
42 | assert response.tag == 5
43 | assert response.result == "success"
44 |
--------------------------------------------------------------------------------