Skip to content

Commit

Permalink
Merge pull request #173 from rmartin16/auto-log-out
Browse files Browse the repository at this point in the history
Document log out options
  • Loading branch information
rmartin16 authored Apr 19, 2023
2 parents c8a8be2 + 2e651c7 commit 7c973dd
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 225 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:
run: docker logs qbt

- name: Store coverage data
if: success() || failure()
uses: actions/[email protected]
with:
name: "coverage-data"
Expand Down
12 changes: 6 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ repos:
- id: isort
additional_dependencies: [toml]

- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.261
hooks:
- id: ruff
args:
- "--fix"

- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.2.0
hooks:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Change log
==========
### v2023.4.47 (19 apr 2023)
- ``Client`` can now be used as a context manager

### v2023.4.46 (14 apr 2023)
- Fix building docs after implementing ``src-layout``

Expand Down
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Installation
------------
Install via pip from [PyPI](https://pypi.org/project/qbittorrent-api/)
```bash
pip install qbittorrent-api
python -m pip install qbittorrent-api
```

Getting Started
Expand All @@ -33,12 +33,13 @@ Getting Started
import qbittorrentapi

# instantiate a Client using the appropriate WebUI configuration
qbt_client = qbittorrentapi.Client(
host='localhost',
conn_info = dict(
host="localhost",
port=8080,
username='admin',
password='adminadmin',
username="admin",
password="adminadmin",
)
qbt_client = qbittorrentapi.Client(**conn_info)

# the Client will automatically acquire/maintain a logged-in state
# in line with any request. therefore, this is not strictly necessary;
Expand All @@ -48,14 +49,24 @@ try:
except qbittorrentapi.LoginFailed as e:
print(e)

# if the Client will not be long-lived or many Clients may be created
# in a relatively short amount of time, be sure to log out:
qbt_client.auth_log_out()

# or use a context manager:
with qbittorrentapi.Client(**conn_info) as qbt_client:
if qbt_client.torrents_add(urls="...") != "Ok.":
raise Exception("Failed to add torrent.")

# display qBittorrent info
print(f'qBittorrent: {qbt_client.app.version}')
print(f'qBittorrent Web API: {qbt_client.app.web_api_version}')
for k,v in qbt_client.app.build_info.items(): print(f'{k}: {v}')
print(f"qBittorrent: {qbt_client.app.version}")
print(f"qBittorrent Web API: {qbt_client.app.web_api_version}")
for k, v in qbt_client.app.build_info.items():
print(f"{k}: {v}")

# retrieve and show all torrents
for torrent in qbt_client.torrents_info():
print(f'{torrent.hash[-6:]}: {torrent.name} ({torrent.state})')
print(f"{torrent.hash[-6:]}: {torrent.name} ({torrent.state})")

# pause all torrents
qbt_client.torrents.pause.all()
Expand Down
46 changes: 30 additions & 16 deletions docs/source/behavior&configuration.rst
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
Behavior & Configuration
================================

Untrusted WebUI Certificate
***************************
* qBittorrent allows you to configure HTTPS with an untrusted certificate; this commonly includes self-signed certificates.
* When using such a certificate, instantiate Client with ``VERIFY_WEBUI_CERTIFICATE=False`` or set environment variable ``QBITTORRENTAPI_DO_NOT_VERIFY_WEBUI_CERTIFICATE`` to a non-null value.
* Failure to do this for will cause connections to qBittorrent to fail.
* As a word of caution, doing this actually does turn off certificate verification. Therefore, for instance, potential man-in-the-middle attacks will not be detected and reported (since the error is suppressed). However, the connection will remain encrypted.

.. code:: python
qbt_client = Client(..., VERIFY_WEBUI_CERTIFICATE=False}
Host, Username and Password
***************************
* The authentication credentials can be provided when instantiating ``Client``:
* The authentication credentials can be provided when instantiating :class:`~qbittorrentapi.client.Client`:

.. code:: python
qbt_client = Client(host="localhost:8080", username='...', password='...')
* The credentials can also be specified after ``Client`` is created but calling ``auth_log_in()`` is not strictly necessary to authenticate the client; this will happen automatically for any API request.
* The credentials can also be specified after :class:`~qbittorrentapi.client.Client` is created but calling :meth:`~qbittorrentapi.auth.AuthAPIMixIn.auth_log_in` is not strictly necessary to authenticate the client; this will happen automatically for any API request.

.. code:: python
Expand All @@ -32,13 +21,38 @@ Host, Username and Password
* ``QBITTORRENTAPI_USERNAME``
* ``QBITTORRENTAPI_PASSWORD``

qBittorrent Session Management
******************************
* Any time a connection is established with qBittorrent, it instantiates a session to manage authentication for all subsequent API requests.
* This client will transparently manage sessions by ensuring the client is always logged in in-line with any API request including requesting a new session upon expiration of an existing session.
* However, each new :class:`~qbittorrentapi.client.Client` instantiation will create a new session in qBittorrent.
* Therefore, if many :class:`~qbittorrentapi.client.Client` instances will be created be sure to call :class:`~qbittorrentapi.auth.AuthAPIMixIn.auth_log_out` for each instance or use a context manager.
* Otherwise, qBittorrent may experience abnormally high memory usage.

.. code:: python
with qbittorrentapi.Client(**conn_info) as qbt_client:
if qbt_client.torrents_add(urls="...") != "Ok.":
raise Exception("Failed to add torrent.")
Untrusted WebUI Certificate
***************************
* qBittorrent allows you to configure HTTPS with an untrusted certificate; this commonly includes self-signed certificates.
* When using such a certificate, instantiate Client with ``VERIFY_WEBUI_CERTIFICATE=False`` or set environment variable ``QBITTORRENTAPI_DO_NOT_VERIFY_WEBUI_CERTIFICATE`` to a non-null value.
* Failure to do this for will cause connections to qBittorrent to fail.
* As a word of caution, doing this actually does turn off certificate verification. Therefore, for instance, potential man-in-the-middle attacks will not be detected and reported (since the error is suppressed). However, the connection will remain encrypted.

.. code:: python
qbt_client = Client(..., VERIFY_WEBUI_CERTIFICATE=False}
Requests Configuration
**********************
* The `Requests <https://requests.readthedocs.io/en/latest/>`_ package is used to issue HTTP requests to qBittorrent to facilitate this API.
* Much of ``Requests`` configuration for making HTTP calls can be controlled with parameters passed along with the request payload.
* Much of `Requests` configuration for making HTTP requests can be controlled with parameters passed along with the request payload.
* For instance, HTTP Basic Authorization credentials can be provided via ``auth``, timeouts via ``timeout``, or Cookies via ``cookies``. See `Requests documentation <https://requests.readthedocs.io/en/latest/api/#requests.request>`_ for full details.
* These parameters are exposed here in two ways; the examples below tell ``Requests`` to use a connect timeout of 3.1 seconds and a read timeout of 30 seconds.
* When you instantiate ``Client``, you can specify the parameters to use in all HTTP requests to qBittorrent:
* When you instantiate :class:`~qbittorrentapi.client.Client`, you can specify the parameters to use in all HTTP requests to qBittorrent:
.. code:: python
Expand Down Expand Up @@ -69,7 +83,7 @@ Additional HTTP Headers
Unimplemented API Endpoints
***************************
* Since the qBittorrent Web API has evolved over time, some endpoints may not be available from the qBittorrent host.
* By default, if a call is made to endpoint that doesn't exist for the version of the qBittorrent host (e.g., the Search endpoints were introduced in Web API v2.1.1), there's a debug logger output and None is returned.
* By default, if a request is made to endpoint that doesn't exist for the version of the qBittorrent host (e.g., the Search endpoints were introduced in Web API v2.1.1), there's a debug logger output and None is returned.
* To raise ``NotImplementedError`` instead, instantiate Client with:
.. code:: python
Expand Down
68 changes: 40 additions & 28 deletions docs/source/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ Installation

.. code:: console
pip install qbittorrent-api
python -m pip install qbittorrent-api
* Install a specific release (e.g. ``v2022.8.34``):

.. code:: console
pip install git+https://github.com/rmartin16/qbittorrent-api.git@v2022.8.34#egg=qbittorrent-api
python -m pip install qbittorrent-api==2022.8.34
* Install direct from ``main``:

Expand All @@ -61,34 +61,46 @@ Getting Started
---------------
.. code:: python
import qbittorrentapi
import qbittorrentapi
# instantiate a Client using the appropriate WebUI configuration
qbt_client = qbittorrentapi.Client(
host='localhost',
port=8080,
username='admin',
password='adminadmin'
# instantiate a Client using the appropriate WebUI configuration
conn_info = dict(
host="localhost",
port=8080,
username="admin",
password="adminadmin",
)
# the Client will automatically acquire/maintain a logged in state in line with any request.
# therefore, this is not necessary; however, you many want to test the provided login credentials.
try:
qbt_client.auth_log_in()
except qbittorrentapi.LoginFailed as e:
print(e)
# display qBittorrent info
print(f'qBittorrent: {qbt_client.app.version}')
print(f'qBittorrent Web API: {qbt_client.app.web_api_version}')
for k,v in qbt_client.app.build_info.items(): print(f'{k}: {v}')
# retrieve and show all torrents
for torrent in qbt_client.torrents_info():
print(f'{torrent.hash[-6:]}: {torrent.name} ({torrent.state})')
# pause all torrents
qbt_client.torrents.pause.all()
qbt_client = qbittorrentapi.Client(**conn_info)
# the Client will automatically acquire/maintain a logged-in state
# in line with any request. therefore, this is not strictly necessary;
# however, you may want to test the provided login credentials.
try:
qbt_client.auth_log_in()
except qbittorrentapi.LoginFailed as e:
print(e)
# if the Client will not be long-lived or many Clients may be created
# in a relatively short amount of time, be sure to log out:
qbt_client.auth_log_out()
# or use a context manager:
with qbittorrentapi.Client(**conn_info) as qbt_client:
if qbt_client.torrents_add(urls="...") != "Ok.":
raise Exception("Failed to add torrent.")
# display qBittorrent info
print(f"qBittorrent: {qbt_client.app.version}")
print(f"qBittorrent Web API: {qbt_client.app.web_api_version}")
for k, v in qbt_client.app.build_info.items():
print(f"{k}: {v}")
# retrieve and show all torrents
for torrent in qbt_client.torrents_info():
print(f"{torrent.hash[-6:]}: {torrent.name} ({torrent.state})")
# pause all torrents
qbt_client.torrents.pause.all()
Usage
-----
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = qbittorrent-api
version = 2023.4.46
version = 2023.4.47
author = Russell Martin
author_email = [email protected]
maintainer = Russell Martin
Expand Down
3 changes: 1 addition & 2 deletions src/qbittorrentapi/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from qbittorrentapi.decorators import login_required
from qbittorrentapi.definitions import APINames
from qbittorrentapi.definitions import ClientCache
from qbittorrentapi.exceptions import HTTP403Error
from qbittorrentapi.exceptions import LoginFailed
from qbittorrentapi.exceptions import UnsupportedQbittorrentVersion
from qbittorrentapi.request import Request
Expand Down Expand Up @@ -75,7 +74,7 @@ def is_logged_in(self):
"""
try:
self._post(_name=APINames.Application, _method="version")
except HTTP403Error:
except Exception:
return False
else:
return True
Expand Down
59 changes: 34 additions & 25 deletions src/qbittorrentapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,31 +61,40 @@ class Client(
>>> client = Client(host='localhost:8080', username='admin', password='adminadmin')
>>> torrents = client.torrents_info()
:param host: hostname for qBittorrent Web API (e.g. ``[http[s]://]localhost[:8080]``)
:param port: port number for qBittorrent Web API (note: only used if host does not contain a port)
:param username: username for qBittorrent client
:param password: password for qBittorrent client
:param SIMPLE_RESPONSES: By default, complex objects are returned from some endpoints. These objects will allow for
accessing responses' items as attributes and include methods for contextually relevant actions.
This comes at the cost of performance. Generally, this cost isn't large; however, some
endpoints, such as ``torrents_files()`` method, may need to convert a large payload.
Set this to True to return the simple JSON back.
Alternatively, set this to True only for an individual method call. For instance, when
requesting the files for a torrent: ``client.torrents_files(hash='...', SIMPLE_RESPONSES=True)``.
:param VERIFY_WEBUI_CERTIFICATE: Set to False to skip verify certificate for HTTPS connections;
for instance, if the connection is using a self-signed certificate. Not setting this to False for self-signed
certs will cause a :class:`qbittorrentapi.exceptions.APIConnectionError` exception to be raised.
:param EXTRA_HEADERS: Dictionary of HTTP Headers to include in all requests made to qBittorrent.
:param REQUESTS_ARGS: Dictionary of configuration for Requests package: `<https://requests.readthedocs.io/en/latest/api/#requests.request>`_
:param FORCE_SCHEME_FROM_HOST: If a scheme (i.e. ``http`` or ``https``) is specified in host, it will be used regardless
of whether qBittorrent is configured for HTTP or HTTPS communication. Normally, this client will attempt to
determine which scheme qBittorrent is actually listening on...but this can cause problems in rare cases.
:param RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: Some Endpoints may not be implemented in older
versions of qBittorrent. Setting this to True will raise a :class:`NotImplementedError` instead of just returning ``None``.
:param RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS: raise the UnsupportedQbittorrentVersion exception if the
connected version of qBittorrent is not fully supported by this client.
:param DISABLE_LOGGING_DEBUG_OUTPUT: Turn off debug output from logging for this package as well as Requests & urllib3.
:param host: hostname for qBittorrent Web API, ``[http[s]://]localhost[:8080][/path]``
:param port: port number for qBittorrent Web API (ignored if host contains a port)
:param username: username for qBittorrent Web API
:param password: password for qBittorrent Web API
:param SIMPLE_RESPONSES: By default, complex objects are returned from some
endpoints. These objects will allow for accessing responses' items as
attributes and include methods for contextually relevant actions. This
comes at the cost of performance. Generally, this cost isn't large;
however, some endpoints, such as ``torrents_files()`` method, may need
to convert a large payload. Set this to True to return the simple JSON
back. Alternatively, set this to True only for an individual method call.
For instance, when requesting the files for a torrent:
``client.torrents_files(hash='...', SIMPLE_RESPONSES=True)``
:param VERIFY_WEBUI_CERTIFICATE: Set to False to skip verify certificate for
HTTPS connections; for instance, if the connection is using a self-signed
certificate. Not setting this to False for self-signed certs will cause a
:class:`~qbittorrentapi.exceptions.APIConnectionError` exception to be raised.
:param EXTRA_HEADERS: Dictionary of HTTP Headers to include in all requests
made to qBittorrent.
:param REQUESTS_ARGS: Dictionary of configuration for Requests package:
`<https://requests.readthedocs.io/en/latest/api/#requests.request>`_
:param FORCE_SCHEME_FROM_HOST: If a scheme (i.e. ``http`` or ``https``) is
specified in host, it will be used regardless of whether qBittorrent is
configured for HTTP or HTTPS communication. Normally, this client will
attempt to determine which scheme qBittorrent is actually listening on...
but this can cause problems in rare cases. Defaults ``False``.
:param RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: Some Endpoints
may not be implemented in older versions of qBittorrent. Setting this to True
will raise a :class:`NotImplementedError` instead of just returning``None``.
:param RAISE_ERROR_FOR_UNSUPPORTED_QBITTORRENT_VERSIONS: raise the
UnsupportedQbittorrentVersion exception if the connected version of
qBittorrent is not fully supported by this client. Defaults ``False``.
:param DISABLE_LOGGING_DEBUG_OUTPUT: Turn off debug output from logging for
this package as well as Requests & urllib3.
""" # noqa: E501

def __init__(
Expand Down
5 changes: 3 additions & 2 deletions src/qbittorrentapi/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,8 @@ def request(self, method, url, **kwargs):

# send Content-Length as 0 for empty POSTs...Requests will not send Content-Length
# if data is empty but qBittorrent will complain otherwise
is_data = any(x is not None for x in kwargs.get("data", {}).values())
data = kwargs.get("data") or {}
is_data = any(x is not None for x in data.values())
if method.lower() == "post" and not is_data:
kwargs.setdefault("headers", {}).update({"Content-Length": "0"})

Expand Down Expand Up @@ -733,7 +734,7 @@ def _trigger_session_initialization(self):
"""
try:
self._http_session.close()
except AttributeError:
except Exception: # noqa: S110
pass
self._http_session = None

Expand Down
Loading

0 comments on commit 7c973dd

Please sign in to comment.