Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-proxmoxer for 
openSUSE:Factory checked in at 2026-03-07 20:09:51
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-proxmoxer (Old)
 and      /work/SRC/openSUSE:Factory/.python-proxmoxer.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-proxmoxer"

Sat Mar  7 20:09:51 2026 rev:6 rq:1337381 version:2.3.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-proxmoxer/python-proxmoxer.changes        
2025-06-11 16:27:30.846946639 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-proxmoxer.new.8177/python-proxmoxer.changes  
    2026-03-07 20:14:42.267717012 +0100
@@ -1,0 +2,14 @@
+Thu Mar 05 07:56:20 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 2.3.0:
+  * Mark supported Python as 3.10-3.14
+  * Only decode response as JSON if call was successful (#204)
+  * Add ability to pass proxy configuration to https backend (#206)
+  * Update 2FA Mechanism (#158)
+  * Update testing to python 3.10 - 3.13 (#214)
+  * Add test for `pvesh` JSON preceded by non-JSON.
+  * Add workaround for broken `pvesh` output.
+  * Adjust tests for responses==0.25.5 changes
+  * Add exit_code to Response from command_base and ResourceException
+
+-------------------------------------------------------------------

Old:
----
  proxmoxer-2.2.0.obscpio

New:
----
  proxmoxer-2.3.0.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-proxmoxer.spec ++++++
--- /var/tmp/diff_new_pack.Y0Aq2Y/_old  2026-03-07 20:14:42.935744646 +0100
+++ /var/tmp/diff_new_pack.Y0Aq2Y/_new  2026-03-07 20:14:42.935744646 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-proxmoxer
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,14 +18,14 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-proxmoxer
-Version:        2.2.0
+Version:        2.3.0
 Release:        0
 Summary:        Python Wrapper for the Proxmox 2x API (HTTP and SSH)
 License:        MIT
 URL:            https://github.com/proxmoxer/proxmoxer/
 # the Pypi tarball does not contain the tests directory
 Source:         proxmoxer-%{version}.tar.gz
-BuildRequires:  %{python_module base >= 3.8}
+BuildRequires:  %{python_module base >= 3.10}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module setuptools}
 BuildRequires:  %{python_module wheel}

++++++ _service ++++++
--- /var/tmp/diff_new_pack.Y0Aq2Y/_old  2026-03-07 20:14:43.003747459 +0100
+++ /var/tmp/diff_new_pack.Y0Aq2Y/_new  2026-03-07 20:14:43.007747625 +0100
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/proxmoxer/proxmoxer</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">2.2.0</param>
+    <param name="revision">2.3.0</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="changesgenerate">enable</param>
   </service>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.Y0Aq2Y/_old  2026-03-07 20:14:43.043749114 +0100
+++ /var/tmp/diff_new_pack.Y0Aq2Y/_new  2026-03-07 20:14:43.051749445 +0100
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/proxmoxer/proxmoxer</param>
-              <param 
name="changesrevision">336a0317123bdd8e1e4a43f789ed289487097f08</param></service></servicedata>
+              <param 
name="changesrevision">99fe9814d6212c614a944ce7b3d907e05042c4fa</param></service></servicedata>
 (No newline at EOF)
 

++++++ proxmoxer-2.2.0.obscpio -> proxmoxer-2.3.0.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/.devcontainer/devcontainer.json 
new/proxmoxer-2.3.0/.devcontainer/devcontainer.json
--- old/proxmoxer-2.2.0/.devcontainer/devcontainer.json 2024-12-15 
03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/.devcontainer/devcontainer.json 2026-03-04 
03:11:21.000000000 +0100
@@ -7,7 +7,7 @@
     "context": "..",
     "args": {
       // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9, 
3.10, 3.11
-      "VARIANT": "3.8"
+      "VARIANT": "3.10"
     }
   },
   // Set *default* container specific settings.json values on container create.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/.git-blame-ignore-revs 
new/proxmoxer-2.3.0/.git-blame-ignore-revs
--- old/proxmoxer-2.2.0/.git-blame-ignore-revs  2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/.git-blame-ignore-revs  1970-01-01 01:00:00.000000000 
+0100
@@ -1,4 +0,0 @@
-# use with `git config blame.ignorerevsfile .git-blame-ignore-revs`
-
-# Format code base with Black
-7a976de985fc7b71fdf31d3161f223eeaada38da
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/.github/workflows/ci.yaml 
new/proxmoxer-2.3.0/.github/workflows/ci.yaml
--- old/proxmoxer-2.2.0/.github/workflows/ci.yaml       2024-12-15 
03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/.github/workflows/ci.yaml       1970-01-01 
01:00:00.000000000 +0100
@@ -1,74 +0,0 @@
-name: CI
-
-on:
-  push:
-
-  pull_request:
-
-  # Allows you to run this workflow manually from the Actions tab
-  workflow_dispatch:
-
-jobs:
-  unit-test:
-    continue-on-error: ${{ github.repository == 'proxmoxer/proxmoxer' }}
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        python-version:
-          - "3.8"
-          - "3.9"
-          - "3.10"
-          - "3.11"
-          - "3.12"
-
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-
-      - name: Set up Python
-        uses: actions/setup-python@v4
-        with:
-          python-version: ${{ matrix.python-version }}
-
-      - name: Cache PIP packages
-        uses: actions/cache@v3
-        with:
-          path: ~/.cache/pip
-          key: ${{ runner.os }}-pip-python${{ matrix.python-version }}-${{ 
hashFiles('*requirements.txt') }}
-          restore-keys: |
-            ${{ runner.os }}-pip-python${{ matrix.python-version }}-
-            ${{ runner.os }}-pip-
-
-      - name: Install pip Packages
-        run: pip install -r test_requirements.txt
-
-      - name: Install Self as Package
-        run: pip install .
-
-      - name: Run Tests
-        run: pytest -v --cov tests/
-
-      - name: Run pre-commit lint/format checks
-        uses: pre-commit/[email protected]
-
-      - name: Upload coverage data to coveralls.io
-        if: github.repository == 'proxmoxer/proxmoxer'
-        run: coveralls --service=github
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          COVERALLS_FLAG_NAME: Unit Test (${{ matrix.python-version }})
-          COVERALLS_PARALLEL: true
-
-
-  complete:
-    name: Finalize Coveralls Report
-    if: github.repository == 'proxmoxer/proxmoxer'
-    needs: unit-test
-    runs-on: ubuntu-latest
-    steps:
-      - name: Coveralls Finished
-        uses: coverallsapp/[email protected]
-        with:
-          parallel-finished: true
-          github-token: ${{ secrets.GITHUB_TOKEN }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/.gitignore 
new/proxmoxer-2.3.0/.gitignore
--- old/proxmoxer-2.2.0/.gitignore      2024-12-15 03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/.gitignore      1970-01-01 01:00:00.000000000 +0100
@@ -1,147 +0,0 @@
-# IDE files
-.idea
-*.code-workspace
-
-coverage.*
-
-# generated files
-README.txt
-
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-#   For a library or package, you might want to ignore these files since the 
code is
-#   intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in 
version control.
-#   However, in case of collaboration, if having platform-specific 
dependencies or dependencies
-#   having no cross-platform support, pipenv may install dependencies that 
don't work, or not
-#   install all needed dependencies.
-#Pipfile.lock
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/.pre-commit-config.yaml 
new/proxmoxer-2.3.0/.pre-commit-config.yaml
--- old/proxmoxer-2.2.0/.pre-commit-config.yaml 2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/.pre-commit-config.yaml 2026-03-04 03:11:21.000000000 
+0100
@@ -1,13 +1,13 @@
 repos:
   ###### FORMATTING ######
   - repo: https://github.com/psf/black-pre-commit-mirror
-    rev: 23.11.0
+    rev: 23.12.1
     hooks:
       - id: black
         language_version: python3 # Should be a command that runs python3.6+
 
   - repo: https://github.com/PyCQA/isort
-    rev: 5.12.0
+    rev: 5.13.2
     hooks:
       - id: isort
         name: isort (python)
@@ -16,14 +16,14 @@
         types: [pyi]
 
   ###### LINTING ######
-  - repo: https://github.com/PyCQA/bandit
-    rev: 1.7.5
-    hooks:
-      - id: bandit
-        args: ["--configfile", ".bandit", "--baseline", 
"tests/known_issues.json"]
+  # - repo: https://github.com/PyCQA/bandit
+  #   rev: 1.7.6
+  #   hooks:
+  #     - id: bandit
+  #       args: ["--configfile", ".bandit", "--baseline", 
"tests/known_issues.json"]
 
   - repo: https://github.com/PyCQA/flake8
-    rev: 6.1.0
+    rev: 7.1.1
     hooks:
       - id: flake8
       # any flake8 plugins must be included in the hook venv
@@ -35,7 +35,7 @@
   #     - id: pylint
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.5.0
+    rev: v5.0.0
     hooks:
       - id: check-case-conflict
       - id: check-symlinks
@@ -46,10 +46,10 @@
         args: [--fix=no]
 
   - repo: https://github.com/asottile/blacken-docs
-    rev: 1.16.0
+    rev: 1.18.0
     hooks:
     -   id: blacken-docs
-        additional_dependencies: [black==23.11.0]
+        additional_dependencies: [black==23.12.1]
 
   - repo: https://github.com/pre-commit/pygrep-hooks
     rev: v1.10.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/CHANGELOG.md 
new/proxmoxer-2.3.0/CHANGELOG.md
--- old/proxmoxer-2.2.0/CHANGELOG.md    2024-12-15 03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/CHANGELOG.md    2026-03-04 03:11:21.000000000 +0100
@@ -1,3 +1,11 @@
+## 2.3.0 (2026-02-07)
+
+* Improvement (all): Add exit_code to Response from command_base and 
ResourceException ([John Hollowell](https://github.com/jhollowe))
+* Improvement (local,openssh,paramiko): Add workaround for broken pvesh output 
([Markus Reiter](https://github.com/reitermarkus))
+* Bugfix (https): Update 2FA to support modern 2-step flow 
([jpattWPC](https://github.com/jpattWPC))
+* Improvement (https): Support direct proxy configuration ([Eric 
Baudach](https://github.com/sniffer32))
+* Bugfix (all): Only decode response as JSON if call was successful ([Michael 
Ablassmeier](https://github.com/abbbi))
+
 ## 2.2.0 (2024-12-13)
 
 * Bugfix (local,openssh,paramiko): Remove IP/hostname from command path 
([Andrea Dainese](https://github.com/dainok), [John 
Hollowell](https://github.com/jhollowe))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/proxmoxer/__init__.py 
new/proxmoxer-2.3.0/proxmoxer/__init__.py
--- old/proxmoxer-2.2.0/proxmoxer/__init__.py   2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/proxmoxer/__init__.py   2026-03-04 03:11:21.000000000 
+0100
@@ -1,6 +1,6 @@
 __author__ = "Oleg Butovich"
 __copyright__ = "(c) Oleg Butovich 2013-2024"
-__version__ = "2.2.0"
+__version__ = "2.3.0"
 __license__ = "MIT"
 
 from .core import *  # noqa
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/proxmoxer/backends/command_base.py 
new/proxmoxer-2.3.0/proxmoxer/backends/command_base.py
--- old/proxmoxer-2.2.0/proxmoxer/backends/command_base.py      2024-12-15 
03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/proxmoxer/backends/command_base.py      2026-03-04 
03:11:21.000000000 +0100
@@ -30,8 +30,9 @@
 
 
 class Response:
-    def __init__(self, content, status_code):
+    def __init__(self, content, status_code, exit_code):
         self.status_code = status_code
+        self.exit_code = exit_code
         self.content = content
         self.text = str(content)
         self.headers = {"content-type": "application/json"}
@@ -110,7 +111,7 @@
         if self.sudo:
             full_cmd = ["sudo"] + full_cmd
 
-        stdout, stderr = self._exec(full_cmd)
+        stdout, stderr, exit_code = self._exec(full_cmd)
 
         def is_http_status_string(s):
             return re.match(r"\d\d\d [a-zA-Z]", str(s))
@@ -135,8 +136,8 @@
         else:
             status_code = 200
         if stdout:
-            return Response(stdout, status_code)
-        return Response(stderr, status_code)
+            return Response(stdout, status_code, exit_code)
+        return Response(stderr, status_code, exit_code)
 
     def upload_file_obj(self, file_obj, remote_path):
         raise NotImplementedError()
@@ -144,10 +145,24 @@
 
 class JsonSimpleSerializer:
     def loads(self, response):
+        # FIXME: Workaround for 
https://bugzilla.proxmox.com/show_bug.cgi?id=4333.
+        #
+        # With each iteration, try parsing one fewer line, until
+        # we reach the beginning of the actual JSON message.
         try:
-            return json.loads(response.content)
-        except (UnicodeDecodeError, ValueError):
-            return {"errors": response.content}
+            content = response.content
+            if isinstance(content, bytes):
+                content = content.decode("utf-8")
+            content_lines = content.splitlines()
+            while content_lines:
+                try:
+                    return json.loads("\n".join(content_lines))
+                except ValueError:
+                    content_lines = content_lines[1:]
+        except UnicodeDecodeError:
+            pass
+
+        return {"errors": response.content}
 
     def loads_errors(self, response):
         try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/proxmoxer/backends/https.py 
new/proxmoxer-2.3.0/proxmoxer/backends/https.py
--- old/proxmoxer-2.2.0/proxmoxer/backends/https.py     2024-12-15 
03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/proxmoxer/backends/https.py     2026-03-04 
03:11:21.000000000 +0100
@@ -42,11 +42,12 @@
     def get_tokens(self):
         return None, None
 
-    def __init__(self, timeout=5, service="PVE", verify_ssl=False, cert=None):
+    def __init__(self, timeout=5, service="PVE", verify_ssl=False, cert=None, 
proxies=None):
         self.timeout = timeout
         self.service = service
         self.verify_ssl = verify_ssl
         self.cert = cert
+        self.proxies = proxies
 
 
 class ProxmoxHTTPAuth(ProxmoxHTTPAuthBase):
@@ -54,45 +55,63 @@
     # if calls are made less frequently than 2 hrs, using the API token auth 
is recommended
     renew_age = 3600
 
-    def __init__(self, username, password, otp=None, base_url="", **kwargs):
+    def __init__(self, username, password, otp=None, base_url="", 
otptype="totp", **kwargs):
         super().__init__(**kwargs)
         self.base_url = base_url
         self.username = username
         self.pve_auth_ticket = ""
 
-        self._get_new_tokens(password=password, otp=otp)
+        self._get_new_tokens(password=password, otp=otp, otptype=otptype)
 
-    def _get_new_tokens(self, password=None, otp=None):
+    def _get_new_tokens(self, password=None, otp=None, otptype=None):
         if password is None:
             # refresh from existing (unexpired) ticket
             password = self.pve_auth_ticket
 
         data = {"username": self.username, "password": password}
-        if otp:
-            data["otp"] = otp
 
-        response_data = requests.post(
+        response = requests.post(
             self.base_url + "/access/ticket",
             verify=self.verify_ssl,
             timeout=self.timeout,
             data=data,
             cert=self.cert,
-        ).json()["data"]
-        if response_data is None:
+            proxies=self.proxies,
+        )
+        if response.status_code != 200:
             raise AuthenticationError(
-                "Couldn't authenticate user: {0} to {1}".format(
-                    self.username, self.base_url + "/access/ticket"
+                "Couldn't authenticate user: {0} to {1} code: {2}".format(
+                    self.username,
+                    self.base_url + "/access/ticket",
+                    response.status_code,
                 )
             )
-        if response_data.get("NeedTFA") is not None:
-            raise AuthenticationError(
-                "Couldn't authenticate user: missing Two Factor Authentication 
(TFA)"
-            )
+        response_data = response.json()["data"]
 
         self.birth_time = time.monotonic()
         self.pve_auth_ticket = response_data["ticket"]
         self.csrf_prevention_token = response_data["CSRFPreventionToken"]
 
+        if response_data.get("NeedTFA") is not None:
+            otpdata = {
+                "username": self.username,
+                "tfa-challenge": self.pve_auth_ticket,
+                "password": f"{otptype}:{otp}",
+            }
+            otpresp = response_data = requests.post(
+                self.base_url + "/access/ticket",
+                verify=self.verify_ssl,
+                timeout=self.timeout,
+                data=otpdata,
+            ).json()["data"]
+            if not otpresp:
+                raise AuthenticationError(
+                    "Couldn't authenticate user: missing Two Factor 
Authentication (TFA)"
+                )
+            self.birth_time = time.monotonic()
+            self.pve_auth_ticket = otpresp["ticket"]
+            self.csrf_prevention_token = otpresp["CSRFPreventionToken"]
+
     def get_cookies(self):
         return cookiejar_from_dict({self.service + "AuthCookie": 
self.pve_auth_ticket})
 
@@ -207,7 +226,11 @@
 
                 # add in filename from file pointer (patch for 
https://github.com/requests/toolbelt/pull/316)
                 # add Content-Type since Proxmox requires it 
(https://bugzilla.proxmox.com/show_bug.cgi?id=4344)
-                files[k] = (requests.utils.guess_filename(v), v, 
"application/octet-stream")
+                files[k] = (
+                    requests.utils.guess_filename(v),
+                    v,
+                    "application/octet-stream",
+                )
                 del data[k]
 
         # if there are any large files, send all data and files using 
streaming multipart encoding
@@ -267,7 +290,9 @@
         path_prefix=None,
         service="PVE",
         cert=None,
+        proxies=None,
     ):
+        self.proxies = proxies
         self.cert = cert
         host_port = ""
         if len(host.split(":")) > 2:  # IPv6
@@ -302,6 +327,7 @@
                 timeout=timeout,
                 service=service,
                 cert=self.cert,
+                proxies=proxies,
             )
         elif password is not None:
             if "password" not in SERVICES[service]["supported_https_auths"]:
@@ -316,6 +342,7 @@
                 timeout=timeout,
                 service=service,
                 cert=self.cert,
+                proxies=proxies,
             )
         else:
             config_failure("No valid authentication credentials were supplied")
@@ -327,6 +354,8 @@
         # cookies are taken from the auth
         session.headers["Connection"] = "keep-alive"
         session.headers["accept"] = self.get_serializer().get_accept_types()
+        if self.proxies:
+            session.proxies.update(self.proxies)
         return session
 
     def get_base_url(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/proxmoxer/backends/local.py 
new/proxmoxer-2.3.0/proxmoxer/backends/local.py
--- old/proxmoxer-2.2.0/proxmoxer/backends/local.py     2024-12-15 
03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/proxmoxer/backends/local.py     2026-03-04 
03:11:21.000000000 +0100
@@ -12,7 +12,7 @@
     def _exec(self, cmd):
         proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
         stdout, stderr = proc.communicate(timeout=self.timeout)
-        return stdout.decode(), stderr.decode()
+        return stdout.decode(), stderr.decode(), proc.returncode
 
     def upload_file_obj(self, file_obj, remote_path):
         with open(remote_path, "wb") as dest_fp:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/proxmoxer/backends/openssh.py 
new/proxmoxer-2.3.0/proxmoxer/backends/openssh.py
--- old/proxmoxer-2.2.0/proxmoxer/backends/openssh.py   2024-12-15 
03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/proxmoxer/backends/openssh.py   2026-03-04 
03:11:21.000000000 +0100
@@ -55,7 +55,7 @@
 
     def _exec(self, cmd):
         ret = self.ssh_client.run(shell_join(cmd), 
forward_ssh_agent=self.forward_ssh_agent)
-        return ret.stdout, ret.stderr
+        return ret.stdout, ret.stderr, ret.returncode
 
     def upload_file_obj(self, file_obj, remote_path):
         self.ssh_client.scp((file_obj,), target=remote_path)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/proxmoxer/backends/ssh_paramiko.py 
new/proxmoxer-2.3.0/proxmoxer/backends/ssh_paramiko.py
--- old/proxmoxer-2.2.0/proxmoxer/backends/ssh_paramiko.py      2024-12-15 
03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/proxmoxer/backends/ssh_paramiko.py      2026-03-04 
03:11:21.000000000 +0100
@@ -63,7 +63,7 @@
         session.exec_command(shell_join(cmd))
         stdout = session.makefile("rb", -1).read().decode()
         stderr = session.makefile_stderr("rb", -1).read().decode()
-        return stdout, stderr
+        return stdout, stderr, session.recv_exit_status()
 
     def upload_file_obj(self, file_obj, remote_path):
         sftp = self.ssh_client.open_sftp()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/proxmoxer/core.py 
new/proxmoxer-2.3.0/proxmoxer/core.py
--- old/proxmoxer-2.2.0/proxmoxer/core.py       2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/proxmoxer/core.py       2026-03-04 03:11:21.000000000 
+0100
@@ -54,7 +54,7 @@
     An Exception thrown when an Proxmox API call failed
     """
 
-    def __init__(self, status_code, status_message, content, errors=None):
+    def __init__(self, status_code, status_message, content, errors=None, 
exit_code=None):
         """
         Create a new ResourceException
 
@@ -71,6 +71,7 @@
         self.status_message = status_message
         self.content = content
         self.errors = errors
+        self.exit_code = exit_code
         if errors is not None:
             content += f" - {errors}"
         message = f"{status_code} {status_message}: {content}".strip()
@@ -151,6 +152,7 @@
                     ),
                     resp.reason,
                     errors=(self._store["serializer"].loads_errors(resp)),
+                    exit_code=resp.exit_code if hasattr(resp, "exit_code") 
else None,
                 )
             else:
                 raise ResourceException(
@@ -159,6 +161,7 @@
                         resp.status_code, 
ANYEVENT_HTTP_STATUS_CODES.get(resp.status_code)
                     ),
                     resp.text,
+                    exit_code=resp.exit_code if hasattr(resp, "exit_code") 
else None,
                 )
         elif 200 <= resp.status_code <= 299:
             return self._store["serializer"].loads(resp)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/setup.py new/proxmoxer-2.3.0/setup.py
--- old/proxmoxer-2.2.0/setup.py        2024-12-15 03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/setup.py        2026-03-04 03:11:21.000000000 +0100
@@ -41,11 +41,11 @@
         "Intended Audience :: System Administrators",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
         "Programming Language :: Python :: 3.12",
+        "Programming Language :: Python :: 3.13",
+        "Programming Language :: Python :: 3.14",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python",
         "Topic :: Software Development :: Libraries :: Python Modules",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/test_requirements.txt 
new/proxmoxer-2.3.0/test_requirements.txt
--- old/proxmoxer-2.2.0/test_requirements.txt   2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/test_requirements.txt   2026-03-04 03:11:21.000000000 
+0100
@@ -1,5 +1,6 @@
 # required libraries for full functionality
-openssh_wrapper
+# openssh_wrapper # library has not been updated in over a decade, so using a 
fork with needed patches
+git+https://github.com/proxmoxer/openssh-wrapper.git@master
 paramiko
 requests
 requests_toolbelt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/tests/api_mock.py 
new/proxmoxer-2.3.0/tests/api_mock.py
--- old/proxmoxer-2.2.0/tests/api_mock.py       2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/tests/api_mock.py       2026-03-04 03:11:21.000000000 
+0100
@@ -8,7 +8,6 @@
 
 import pytest
 import responses
-from requests_toolbelt import MultipartEncoder
 
 
 @pytest.fixture()
@@ -141,8 +140,6 @@
     def _cb_echo(self, request):
         body = request.body
         if body is not None:
-            if isinstance(body, MultipartEncoder):
-                body = body.to_string()  # really, to byte string
             body = body if isinstance(body, str) else str(body, "utf-8")
 
         resp = {
@@ -166,7 +163,7 @@
                 json.dumps({"data": None}),
             )
         # if this user requires OTP and it is not included
-        if form_data_dict.get("username") == "otp" and 
form_data_dict.get("otp") is None:
+        if form_data_dict.get("username") == "otp" and "tfa-challenge" not in 
form_data_dict:
             return (
                 200,
                 self.common_headers,
@@ -180,7 +177,30 @@
                     }
                 ),
             )
-
+        # if OTP key is not valid
+        elif (
+            form_data_dict.get("username") == "otp"
+            and form_data_dict.get("tfa-challenge") == "otp_ticket"
+        ):
+            if form_data_dict.get("password") == "totp:123456":
+                return (
+                    200,
+                    self.common_headers,
+                    json.dumps(
+                        {
+                            "data": {
+                                "ticket": "new_ticket",
+                                "CSRFPreventionToken": "CSRFPreventionToken_2",
+                            }
+                        }
+                    ),
+                )
+            else:
+                return (
+                    401,
+                    self.common_headers,
+                    json.dumps({"data": None}),
+                )
         # if this is the first ticket
         if form_data_dict.get("password") != "ticket":
             return (
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/tests/test_command_base.py 
new/proxmoxer-2.3.0/tests/test_command_base.py
--- old/proxmoxer-2.2.0/tests/test_command_base.py      2024-12-15 
03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/tests/test_command_base.py      2026-03-04 
03:11:21.000000000 +0100
@@ -16,11 +16,12 @@
 
 class TestResponse:
     def test_init_all_args(self):
-        resp = command_base.Response(b"content", 200)
+        resp = command_base.Response(b"content", 200, 201)
 
         assert resp.content == b"content"
         assert resp.text == "b'content'"
         assert resp.status_code == 200
+        assert resp.exit_code == 201
         assert resp.headers == {"content-type": "application/json"}
         assert str(resp) == "Response (200) b'content'"
 
@@ -48,6 +49,7 @@
         resp = self._session.request("GET", self.base_url + "/fake/echo")
 
         assert resp.status_code == 200
+        assert resp.exit_code == 201
         assert resp.content == [
             "pvesh",
             "get",
@@ -60,6 +62,7 @@
         resp = self._session.request("GET", self.base_url + "/stdout")
 
         assert resp.status_code == 200
+        assert resp.exit_code == 201
         assert (
             resp.content == 
"UPID:node:003094EA:095F1EFE:63E88772:download:file.iso:root@pam:done"
         )
@@ -67,6 +70,7 @@
         resp_stderr = self._session.request("GET", self.base_url + "/stderr")
 
         assert resp_stderr.status_code == 200
+        assert resp.exit_code == 201
         assert (
             resp_stderr.content
             == 
"UPID:node:003094EA:095F1EFE:63E88772:download:file.iso:root@pam:done"
@@ -79,6 +83,7 @@
         )
 
         assert resp.status_code == 403
+        assert resp.exit_code == 501
         assert (
             resp.content
             == 
"pvesh\nget\nhttps://1.2.3.4:1234/api2/json/fake/echo\n-thing\n403 
Unauthorized\n--output-format\njson"
@@ -88,6 +93,7 @@
         resp = self._session.request("GET", self.base_url + "/fake/echo", 
data={"thing": "failure"})
 
         assert resp.status_code == 500
+        assert resp.exit_code == 501
         assert (
             resp.content
             == 
"pvesh\nget\nhttps://1.2.3.4:1234/api2/json/fake/echo\n-thing\nfailure\n--output-format\njson";
@@ -99,6 +105,7 @@
         )
 
         assert resp.status_code == 200
+        assert resp.exit_code == 201
         assert resp.content == [
             "sudo",
             "pvesh",
@@ -112,6 +119,7 @@
         resp = self._session.request("GET", self.base_url + "/fake/echo", 
data={"key": "value"})
 
         assert resp.status_code == 200
+        assert resp.exit_code == 201
         assert resp.content == [
             "pvesh",
             "get",
@@ -128,6 +136,7 @@
         )
 
         assert resp.status_code == 200
+        assert resp.exit_code == 201
         assert resp.content == [
             "pvesh",
             "get",
@@ -146,6 +155,7 @@
         )
 
         assert resp.status_code == 200
+        assert resp.exit_code == 201
         assert resp.content == [
             "pvesh",
             "create",
@@ -166,6 +176,7 @@
         )
 
         assert resp.status_code == 200
+        assert resp.exit_code == 201
         assert resp.content == [
             "pvesh",
             "create",
@@ -187,6 +198,7 @@
             )
 
             assert resp.status_code == 200
+            assert resp.exit_code == 201
             assert resp.content == [
                 "pvesh",
                 "create",
@@ -209,7 +221,7 @@
         input_str = '{"key1": "value1", "key2": "value2"}'
         exp_output = {"key1": "value1", "key2": "value2"}
 
-        response = command_base.Response(input_str.encode("utf-8"), 200)
+        response = command_base.Response(input_str.encode("utf-8"), 200, 201)
 
         act_output = self._serializer.loads(response)
 
@@ -219,7 +231,7 @@
         input_str = "There was an error with the request"
         exp_output = {"errors": b"There was an error with the request"}
 
-        response = command_base.Response(input_str.encode("utf-8"), 200)
+        response = command_base.Response(input_str.encode("utf-8"), 200, 201)
 
         act_output = self._serializer.loads(response)
 
@@ -229,7 +241,20 @@
         input_str = '{"data": {"key1": "value1", "key2": "value2"}, "errors": 
{}}\x80'
         exp_output = {"errors": input_str.encode("utf-8")}
 
-        response = command_base.Response(input_str.encode("utf-8"), 200)
+        response = command_base.Response(input_str.encode("utf-8"), 200, 201)
+
+        act_output = self._serializer.loads(response)
+
+        assert act_output == exp_output
+
+    def test_loads_json_preceded_by_non_json(self):
+        input_str = """
+        virtio0: successfully created disk 
'local-zfs:vm-7777-disk-0,discard=on,iothread=1,size=4G'
+        "UPID:net2-pve:002605B4:00FB48C2:62B9E7EB:qmcreate:7777:root@pam:"
+        """
+        exp_output = 
"UPID:net2-pve:002605B4:00FB48C2:62B9E7EB:qmcreate:7777:root@pam:"
+
+        response = command_base.Response(input_str.encode("utf-8"), 200, 201)
 
         act_output = self._serializer.loads(response)
 
@@ -267,21 +292,21 @@
         "import tempfile; import sys; tf = tempfile.NamedTemporaryFile(); 
sys.stdout.write(tf.name)",
     ]:
         return b"/tmp/tmpasdfasdf", None
-    return cmd, None
+    return cmd, None, 201
 
 
 @classmethod
 def _exec_err(_, cmd):
-    return None, "\n".join(cmd)
+    return None, "\n".join(cmd), 501
 
 
 @classmethod
 def _exec_task(_, cmd):
     upid = 
"UPID:node:003094EA:095F1EFE:63E88772:download:file.iso:root@pam:done"
     if "stderr" in cmd[2]:
-        return None, upid
+        return None, upid, 501
     else:
-        return upid, None
+        return upid, None, 201
 
 
 @classmethod
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/tests/test_core.py 
new/proxmoxer-2.3.0/tests/test_core.py
--- old/proxmoxer-2.2.0/tests/test_core.py      2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/tests/test_core.py      2026-03-04 03:11:21.000000000 
+0100
@@ -58,6 +58,16 @@
             == "ResourceException('500 Internal Error: Unable to do the thing 
- functionality not found')"
         )
 
+    def test_init_exit_code(self):
+        e = core.ResourceException(500, "Internal Error", "Unable to do the 
thing", exit_code=255)
+
+        assert e.status_code == 500
+        assert e.status_message == "Internal Error"
+        assert e.content == "Unable to do the thing"
+        assert e.exit_code == 255
+        assert str(e) == "500 Internal Error: Unable to do the thing"
+        assert repr(e) == "ResourceException('500 Internal Error: Unable to do 
the thing')"
+
 
 class TestProxmoxResource:
     obj = core.ProxmoxResource()
@@ -183,6 +193,7 @@
             ),
         ]
         assert exc_info.value.status_code == 500
+        assert exc_info.value.exit_code == 501
         assert exc_info.value.status_message == "Internal Server Error"
         assert exc_info.value.content == str(b"this is the error")
         assert exc_info.value.errors is None
@@ -206,6 +217,7 @@
             ),
         ]
         assert exc_info.value.status_code == 500
+        assert exc_info.value.exit_code == 501
         assert exc_info.value.status_message == "Internal Server Error"
         assert exc_info.value.content == "this is the reason"
         assert exc_info.value.errors == {"errors": b"this is the error"}
@@ -380,12 +392,12 @@
         self.url = url
 
         if "fail" in url:
-            r = Response(b"this is the error", 500)
+            r = Response(b"this is the error", 500, 501)
             if "reason" in url:
                 r.reason = "this is the reason"
             return r
         else:
-            return Response(b'{"data": {"key": "value"}}', 200)
+            return Response(b'{"data": {"key": "value"}}', 200, 201)
 
 
 @pytest.fixture
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/tests/test_https.py 
new/proxmoxer-2.3.0/tests/test_https.py
--- old/proxmoxer-2.2.0/tests/test_https.py     2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/tests/test_https.py     2026-03-04 03:11:21.000000000 
+0100
@@ -38,6 +38,14 @@
 
         assert str(exc_info.value) == "No valid authentication credentials 
were supplied"
 
+    def test_init_with_proxy(self):
+        proxy_url = "http://proxy.example.com:8080";
+        proxies = {"http": proxy_url, "https": proxy_url}
+        backend = https.Backend("1.2.3.4:1234", token_name="", proxies=proxies)
+
+        session = backend.get_session()
+        assert session.proxies == proxies
+
     def test_init_ip4_separate_port(self):
         backend = https.Backend("1.2.3.4", port=1234, token_name="")
         exp_base_url = "https://1.2.3.4:1234/api2/json";
@@ -198,7 +206,7 @@
         auth = https.ProxmoxHTTPAuth(
             "otp",
             "password",
-            otp="otp",
+            otp="123456",
             base_url=self.base_url,
             service="PMG",
             timeout=1234,
@@ -206,8 +214,8 @@
         )
 
         assert auth.username == "otp"
-        assert auth.pve_auth_ticket == "ticket"
-        assert auth.csrf_prevention_token == "CSRFPreventionToken"
+        assert auth.pve_auth_ticket == "new_ticket"
+        assert auth.csrf_prevention_token == "CSRFPreventionToken_2"
         assert auth.service == "PMG"
         assert auth.timeout == 1234
         assert auth.verify_ssl is True
@@ -239,11 +247,7 @@
 
         assert (
             str(exc_info.value)
-            == f"Couldn't authenticate user: bad_auth to 
{self.base_url}/access/ticket"
-        )
-        assert (
-            repr(exc_info.value)
-            == f'AuthenticationError("Couldn\'t authenticate user: bad_auth to 
{self.base_url}/access/ticket")'
+            == f"Couldn't authenticate user: bad_auth to 
{self.base_url}/access/ticket code: 401"
         )
 
     def test_auth_otp(self, mock_pve):
@@ -358,7 +362,7 @@
         assert m is not None  # content matches multipart for the created file
         assert content["headers"]["Content-Type"] == "multipart/form-data; 
boundary=" + m[1]
 
-    def test_request_streaming(self, toolbelt_on_off, caplog, mock_pve):
+    def test_request_streaming(self, shrink_thresholds, toolbelt_on_off, 
caplog, mock_pve):
         caplog.set_level(logging.INFO, logger=MODULE_LOGGER_NAME)
 
         size = https.STREAMING_SIZE_THRESHOLD + 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/tests/test_local.py 
new/proxmoxer-2.3.0/tests/test_local.py
--- old/proxmoxer-2.2.0/tests/test_local.py     2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/tests/test_local.py     2026-03-04 03:11:21.000000000 
+0100
@@ -54,7 +54,8 @@
             'import sys; sys.stdout.write("stdout content"); 
sys.stderr.write("stderr content")',
         ]
 
-        stdout, stderr = self._session._exec(cmd)
+        stdout, stderr, exit_code = self._session._exec(cmd)
 
         assert stdout == "stdout content"
         assert stderr == "stderr content"
+        assert exit_code == 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/tests/test_openssh.py 
new/proxmoxer-2.3.0/tests/test_openssh.py
--- old/proxmoxer-2.2.0/tests/test_openssh.py   2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/tests/test_openssh.py   2026-03-04 03:11:21.000000000 
+0100
@@ -53,10 +53,11 @@
             "world",
         ]
 
-        stdout, stderr = mock_session._exec(cmd)
+        stdout, stderr, exit_code = mock_session._exec(cmd)
 
         assert stdout == "stdout content"
         assert stderr == "stderr content"
+        assert exit_code == 0
         mock_session.ssh_client.run.assert_called_once_with(
             "echo hello world",
             forward_ssh_agent=True,
@@ -83,7 +84,7 @@
 
     ssh_conn.run = mock.Mock(
         # spec=openssh_wrapper.SSHConnection.run,
-        return_value=mock.Mock(stdout="stdout content", stderr="stderr 
content"),
+        return_value=mock.Mock(stdout="stdout content", stderr="stderr 
content", returncode=0),
     )
 
     ssh_conn.scp = mock.Mock()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/tests/test_paramiko.py 
new/proxmoxer-2.3.0/tests/test_paramiko.py
--- old/proxmoxer-2.2.0/tests/test_paramiko.py  2024-12-15 03:12:42.000000000 
+0100
+++ new/proxmoxer-2.3.0/tests/test_paramiko.py  2026-03-04 03:11:21.000000000 
+0100
@@ -100,10 +100,11 @@
         sess = ssh_paramiko.SshParamikoSession("host", "user")
         sess.ssh_client = mock_client
 
-        stdout, stderr = sess._exec(["echo", "hello", "world"])
+        stdout, stderr, exit_code = sess._exec(["echo", "hello", "world"])
 
         assert stdout == "stdout contents"
         assert stderr == "stderr contents"
+        assert exit_code == 0
         mock_session.exec_command.assert_called_once_with("echo hello world")
 
     def test_upload_file_obj(self, mock_ssh_client):
@@ -147,6 +148,7 @@
     mock_stderr.read.return_value = b"stderr contents"
     mock_channel.makefile.return_value = mock_stdout
     mock_channel.makefile_stderr.return_value = mock_stderr
+    mock_channel.recv_exit_status.return_value = 0
 
     mock_transport.open_session.return_value = mock_channel
     mock_client.get_transport.return_value = mock_transport
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/proxmoxer-2.2.0/tests/tools/test_tasks.py 
new/proxmoxer-2.3.0/tests/tools/test_tasks.py
--- old/proxmoxer-2.2.0/tests/tools/test_tasks.py       2024-12-15 
03:12:42.000000000 +0100
+++ new/proxmoxer-2.3.0/tests/tools/test_tasks.py       2026-03-04 
03:11:21.000000000 +0100
@@ -17,7 +17,8 @@
         caplog.set_level(logging.DEBUG, logger="proxmoxer.core")
 
         status = Tasks.blocking_status(
-            mocked_prox, 
"UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done"
+            mocked_prox,
+            "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done",
         )
 
         assert status == {
@@ -81,7 +82,8 @@
         caplog.set_level(logging.DEBUG, logger="proxmoxer.core")
 
         status = Tasks.blocking_status(
-            mocked_prox, 
"UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:stopped"
+            mocked_prox,
+            
"UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:stopped",
         )
 
         assert status == {
@@ -115,11 +117,12 @@
         status = Tasks.blocking_status(
             mocked_prox,
             
"UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:keep-running",
-            timeout=0.021,
+            timeout=0.023,
             polling_interval=0.01,
         )
 
         assert status is None
+        # assert it polls 3 times (1 initial + floor(timeout/polling_interval) 
attempts)
         assert caplog.record_tuples == [
             (
                 "proxmoxer.core",
@@ -200,7 +203,10 @@
 
 class TestDecodeLog:
     def test_basic(self):
-        log_list = [{"n": 1, "t": "client connection: 127.0.0.1:49608"}, {"t": 
"TASK OK", "n": 2}]
+        log_list = [
+            {"n": 1, "t": "client connection: 127.0.0.1:49608"},
+            {"t": "TASK OK", "n": 2},
+        ]
         log_str = Tasks.decode_log(log_list)
 
         assert log_str == "client connection: 127.0.0.1:49608\nTASK OK"
@@ -212,7 +218,11 @@
         assert log_str == ""
 
     def test_unordered(self):
-        log_list = [{"n": 3, "t": "third"}, {"t": "first", "n": 1}, {"t": 
"second", "n": 2}]
+        log_list = [
+            {"n": 3, "t": "third"},
+            {"t": "first", "n": 1},
+            {"t": "second", "n": 2},
+        ]
         log_str = Tasks.decode_log(log_list)
 
         assert log_str == "first\nsecond\nthird"

++++++ proxmoxer.obsinfo ++++++
--- /var/tmp/diff_new_pack.Y0Aq2Y/_old  2026-03-07 20:14:43.251757718 +0100
+++ /var/tmp/diff_new_pack.Y0Aq2Y/_new  2026-03-07 20:14:43.251757718 +0100
@@ -1,5 +1,5 @@
 name: proxmoxer
-version: 2.2.0
-mtime: 1734228762
-commit: 336a0317123bdd8e1e4a43f789ed289487097f08
+version: 2.3.0
+mtime: 1772590281
+commit: 99fe9814d6212c614a944ce7b3d907e05042c4fa
 

Reply via email to