Skip to content

Commit a09bdb5

Browse files
bobokunNooNameRpre-commit-ci[bot]
authored
4.1.17 (#744)
* 4.1.17-develop1 * Retry on ConnectionError (#740) Add Retries for connection to qbit * Adds !ENV constructor to read environment variables * Update config sample to include ENV variable examples * Fixes #702 * remove warning when remote_dir not defined * [pre-commit.ci] pre-commit autoupdate (#742) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pycqa/isort: 5.13.2 → 6.0.0](PyCQA/isort@5.13.2...6.0.0) - [github.com/psf/black: 24.10.0 → 25.1.0](psf/black@24.10.0...25.1.0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * add more !ENV usage in config.yml.sample * 4.1.17 * formatting --------- Co-authored-by: Denys Kozhevnikov <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 99cfac5 commit a09bdb5

File tree

9 files changed

+99
-29
lines changed

9 files changed

+99
-29
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ repos:
3232
- id: yamlfix
3333
exclude: ^.github/
3434
- repo: https://github.com/pycqa/isort
35-
rev: 5.13.2
35+
rev: 6.0.0
3636
hooks:
3737
- id: isort
3838
name: isort (python)
@@ -43,7 +43,7 @@ repos:
4343
- id: pyupgrade
4444
args: [--py3-plus]
4545
- repo: https://github.com/psf/black
46-
rev: 24.10.0
46+
rev: 25.1.0
4747
hooks:
4848
- id: black
4949
language_version: python3

CHANGELOG

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
# Requirements Updated
2-
ruamel.yaml==0.18.10
3-
41
# New Updates
5-
- Adds support for wlidcard matching in category (Adds #695)
2+
- Adds support for environment variables in config file using (`!ENV VAR_NAME`)
3+
- Add Retries on ConnectionError (#740)
4+
- Fixes #702
5+
- Removes warning when `remote_dir` is not defined
66

7-
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.1.15...v4.1.16
7+
Special thanks to @NooNameR for their contributions!
8+
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.1.16...v4.1.17

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4.1.16
1+
4.1.17

config/config.yml.sample

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ commands:
2020

2121
qbt:
2222
# qBittorrent parameters
23+
# Pass environment variables to the config via !ENV tag
2324
host: "localhost:8080"
24-
user: "username"
25-
pass: "password"
25+
user: !ENV QBIT_USER
26+
pass: !ENV QBIT_PASS
2627

2728
settings:
2829
force_auto_tmm: False # Will force qBittorrent to enable Automatic Torrent Management for each torrent.
@@ -295,7 +296,7 @@ notifiarr:
295296
# Notifiarr integration with webhooks
296297
# Leave Empty/Blank to disable
297298
# Mandatory to fill out API Key
298-
apikey: ####################################
299+
apikey: !ENV NOTIFIARR_API
299300
# <OPTIONAL> Set to a unique value (could be your username on notifiarr for example)
300301
instance:
301302

docs/Config-Setup.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
The script utilizes a YAML config file to load information to connect to the various APIs you can connect with.
55

6-
By default, the script looks at /config/config.yml for the Configuration File unless otherwise specified.
6+
By default, the script looks at `/config/config.yml` when running locally or `/app/config.yml` in docker for the Configuration File unless otherwise specified.
77

88
A template Configuration File can be found in the repo [config/config.yml.sample](https://github.com/StuffAnThings/qbit_manage/blob/master/config/config.yml.sample).
99

10+
You can reference environment variables inside your `config.yml` by `!ENV VAR_NAME`
11+
1012
**WARNING**: As this software is constantly evolving and this wiki might not be up to date the sample shown here might not might not be current. Please refer to the repo for the most current version.
1113

1214
# Config File

modules/config.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -657,16 +657,31 @@ def _sort_share_limits(share_limits):
657657
self.util.check_for_attribute(self.data, "root_dir", parent="directory", default_is_none=True), ""
658658
)
659659
self.remote_dir = os.path.join(
660-
self.util.check_for_attribute(self.data, "remote_dir", parent="directory", default=self.root_dir), ""
660+
self.util.check_for_attribute(
661+
self.data, "remote_dir", parent="directory", default=self.root_dir, do_print=False, save=False
662+
),
663+
"",
661664
)
662665
if self.commands["cross_seed"] or self.commands["tag_nohardlinks"] or self.commands["rem_orphaned"]:
663666
self.remote_dir = self.util.check_for_attribute(
664-
self.data, "remote_dir", parent="directory", var_type="path", default=self.root_dir
667+
self.data,
668+
"remote_dir",
669+
parent="directory",
670+
var_type="path",
671+
default=self.root_dir,
672+
do_print=False,
673+
save=False,
665674
)
666675
else:
667676
if self.recyclebin["enabled"]:
668677
self.remote_dir = self.util.check_for_attribute(
669-
self.data, "remote_dir", parent="directory", var_type="path", default=self.root_dir
678+
self.data,
679+
"remote_dir",
680+
parent="directory",
681+
var_type="path",
682+
default=self.root_dir,
683+
do_print=False,
684+
save=False,
670685
)
671686
if not self.remote_dir:
672687
self.remote_dir = self.root_dir
@@ -754,20 +769,32 @@ def _sort_share_limits(share_limits):
754769
# Connect to Qbittorrent
755770
self.qbt = None
756771
if "qbt" in self.data:
757-
logger.info("Connecting to Qbittorrent...")
758-
self.qbt = Qbt(
759-
self,
760-
{
761-
"host": self.util.check_for_attribute(self.data, "host", parent="qbt", throw=True),
762-
"username": self.util.check_for_attribute(self.data, "user", parent="qbt", default_is_none=True),
763-
"password": self.util.check_for_attribute(self.data, "pass", parent="qbt", default_is_none=True),
764-
},
765-
)
772+
self.qbt = self.__connect()
766773
else:
767774
e = "Config Error: qbt attribute not found"
768775
self.notify(e, "Config")
769776
raise Failed(e)
770777

778+
def __retry_on_connect(exception):
779+
return isinstance(exception.__cause__, ConnectionError)
780+
781+
@retry(
782+
retry_on_exception=__retry_on_connect,
783+
stop_max_attempt_number=5,
784+
wait_exponential_multiplier=30000,
785+
wait_exponential_max=120000,
786+
)
787+
def __connect(self):
788+
logger.info("Connecting to Qbittorrent...")
789+
return Qbt(
790+
self,
791+
{
792+
"host": self.util.check_for_attribute(self.data, "host", parent="qbt", throw=True),
793+
"username": self.util.check_for_attribute(self.data, "user", parent="qbt", default_is_none=True),
794+
"password": self.util.check_for_attribute(self.data, "pass", parent="qbt", default_is_none=True),
795+
},
796+
)
797+
771798
# Empty old files from recycle bin or orphaned
772799
def cleanup_dirs(self, location):
773800
num_del = 0

modules/core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""
2-
modules.core contains all the core functions of qbit_manage such as updating categories/tags etc..
2+
modules.core contains all the core functions of qbit_manage such as updating categories/tags etc..
33
"""

modules/qbittorrent.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from fnmatch import fnmatch
66
from functools import cache
77

8+
from qbittorrentapi import APIConnectionError
89
from qbittorrentapi import Client
910
from qbittorrentapi import LoginFailed
1011
from qbittorrentapi import NotFound404Error
@@ -77,6 +78,9 @@ def __init__(self, config, params):
7778
ex = "Qbittorrent Error: Failed to login. Invalid username/password."
7879
self.config.notify(ex, "Qbittorrent")
7980
raise Failed(ex)
81+
except APIConnectionError as exc:
82+
self.config.notify(exc, "Qbittorrent")
83+
raise Failed(exc) from ConnectionError(exc)
8084
except Exception as exc:
8185
self.config.notify(exc, "Qbittorrent")
8286
raise Failed(exc)

modules/util.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
""" Utility functions for qBit Manage. """
1+
"""Utility functions for qBit Manage."""
22

33
import json
44
import logging
@@ -12,6 +12,7 @@
1212
import requests
1313
import ruamel.yaml
1414
from pytimeparse2 import parse
15+
from ruamel.yaml.constructor import ConstructorError
1516

1617
logger = logging.getLogger("qBit Manage")
1718

@@ -756,13 +757,19 @@ def human_readable_size(size, decimal_places=3):
756757

757758

758759
class YAML:
759-
"""Class to load and save yaml files"""
760+
"""Class to load and save yaml files with !ENV tag preservation and environment variable resolution"""
760761

761762
def __init__(self, path=None, input_data=None, check_empty=False, create=False):
762763
self.path = path
763764
self.input_data = input_data
764765
self.yaml = ruamel.yaml.YAML()
765766
self.yaml.indent(mapping=2, sequence=2)
767+
768+
# Add constructor for !ENV tag
769+
self.yaml.Constructor.add_constructor("!ENV", self._env_constructor)
770+
# Add representer for !ENV tag
771+
self.yaml.Representer.add_representer(EnvStr, self._env_representer)
772+
766773
try:
767774
if input_data:
768775
self.data = self.yaml.load(input_data)
@@ -784,8 +791,36 @@ def __init__(self, path=None, input_data=None, check_empty=False, create=False):
784791
raise Failed("YAML Error: File is empty")
785792
self.data = {}
786793

794+
def _env_constructor(self, loader, node):
795+
"""Constructor for !ENV tag"""
796+
value = loader.construct_scalar(node)
797+
# Resolve the environment variable at runtime
798+
env_value = os.getenv(value)
799+
if env_value is None:
800+
raise ConstructorError(f"Environment variable '{value}' not found")
801+
# Return a custom string subclass that preserves the !ENV tag
802+
return EnvStr(value, env_value)
803+
804+
def _env_representer(self, dumper, data):
805+
"""Representer for EnvStr class"""
806+
return dumper.represent_scalar("!ENV", data.env_var)
807+
787808
def save(self):
788-
"""Save yaml file"""
809+
"""Save yaml file with !ENV tags preserved"""
789810
if self.path:
790-
with open(self.path, "w") as filepath:
811+
with open(self.path, "w", encoding="utf-8") as filepath:
791812
self.yaml.dump(self.data, filepath)
813+
814+
815+
class EnvStr(str):
816+
"""Custom string subclass to preserve !ENV tags"""
817+
818+
def __new__(cls, env_var, resolved_value):
819+
# Create a new string instance with the resolved value
820+
instance = super().__new__(cls, resolved_value)
821+
instance.env_var = env_var # Store the environment variable name
822+
return instance
823+
824+
def __repr__(self):
825+
"""Return the resolved value as a string"""
826+
return super().__repr__()

0 commit comments

Comments
 (0)