New Upstream Release - pyzabbix

Ready changes

Summary

Merged new upstream version: 1.3.0 (was: 1.2.1).

Resulting package

Built on 2023-07-19T16:36 (took 5m38s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-pyzabbix

Lintian Result

Diff

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9b42263
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,121 @@
+name: CI
+
+on:
+  push:
+    tags: ["*.*.*"]
+    branches: [master]
+  pull_request:
+    branches: [master]
+
+  schedule:
+    - cron: 37 1 * * 1
+
+jobs:
+  pre-commit:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - uses: actions/setup-python@v4
+        with:
+          python-version: 3.x
+
+      - uses: pre-commit/action@v3.0.0
+
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+
+      - uses: actions/setup-python@v4
+        with:
+          python-version: 3.x
+
+      - uses: actions/cache@v3
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-
+
+      - run: make install
+      - run: make lint
+
+  test:
+    runs-on: ubuntu-20.04
+    strategy:
+      matrix:
+        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - uses: actions/cache@v3
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-
+
+      - run: make install
+      - run: make test
+
+  e2e:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        zabbix-version: ["4.0", "5.0", "6.0", "6.4"]
+
+    env:
+      ZABBIX_VERSION: ${{ matrix.zabbix-version }}
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - uses: actions/setup-python@v4
+        with:
+          python-version: 3.x
+
+      - uses: actions/cache@v3
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-
+
+      - run: docker-compose up -d
+      - run: docker-compose images
+      - run: make install
+      - run: make e2e
+
+  publish:
+    if: startsWith(github.ref, 'refs/tags')
+    needs: [pre-commit, lint, test, e2e]
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - uses: actions/setup-python@v4
+        with:
+          python-version: 3.x
+
+      - uses: actions/cache@v3
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-
+
+      - run: make clean build
+
+      - if: github.repository_owner == 'lukecyca'
+        uses: pypa/gh-action-pypi-publish@v1.5.0
+        with:
+          password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index cd45890..02eb1f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,167 @@
-build
+## Custom .gitignore
+################################################################################
+
+## Github Python .gitignore
+## See https://github.com/github/gitignore/blob/main/Python.gitignore
+################################################################################
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
 dist/
-pyzabbix.egg-info/
-pyzabbix/*.pyc
+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
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__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/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..8e10645
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,52 @@
+---
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.4.0
+    hooks:
+      - id: check-added-large-files
+      - id: check-case-conflict
+      - id: check-executables-have-shebangs
+      - id: check-shebang-scripts-are-executable
+      - id: check-symlinks
+      - id: destroyed-symlinks
+
+      - id: check-json
+      - id: check-yaml
+      - id: check-toml
+
+      - id: check-merge-conflict
+      - id: end-of-file-fixer
+      - id: mixed-line-ending
+      - id: trailing-whitespace
+
+      - id: name-tests-test
+
+  - repo: https://github.com/pre-commit/mirrors-prettier
+    rev: v2.7.1
+    hooks:
+      - id: prettier
+        files: \.(md|yml|yaml|json)$
+
+  - repo: https://github.com/codespell-project/codespell
+    rev: v2.2.4
+    hooks:
+      - id: codespell
+        exclude: ^examples/additemcsv.py$
+
+  - repo: https://github.com/pycqa/isort
+    rev: 5.12.0
+    hooks:
+      - id: isort
+
+  - repo: https://github.com/psf/black
+    rev: 23.3.0
+    hooks:
+      - id: black
+
+  - repo: https://github.com/asottile/pyupgrade
+    rev: v3.3.1
+    hooks:
+      - id: pyupgrade
+        args: [--py3-plus, --py36-plus]
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index aed400a..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-language: python
-python:
-  - "2.7"
-  - "3.4"
-  - "3.5"
-  - "3.6"
-  - "3.7"
-install:
-  - pip install requests
-script:  python setup.py nosetests
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..db6e272
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,42 @@
+<a name="1.2.1"></a>
+
+## [1.2.1](https://github.com/lukecyca/pyzabbix/compare/1.2.0...1.2.1) (2022-08-25)
+
+### :bug: Bug Fixes
+
+- improve deprecation message for confimport
+
+<a name="1.2.0"></a>
+
+## [1.2.0](https://github.com/lukecyca/pyzabbix/compare/1.1.0...1.2.0) (2022-08-04)
+
+### :bug: Bug Fixes
+
+- catch ValueError during json parsing
+
+### :rocket: Features
+
+- parse version using packaging.version
+
+<a name="1.1.0"></a>
+
+## [1.1.0](https://github.com/lukecyca/pyzabbix/compare/1.0.0...1.1.0) (2022-07-28)
+
+### :bug: Bug Fixes
+
+- api object/method attributes should be private
+- auto correct server url trailing slash
+
+### :rocket: Features
+
+- replace custom handler with logging.NullHandler
+- package is typed
+- deprecate ZabbixAPI.confimport alias
+- allow creating calls using dict syntax
+- replace dynamic func with ZabbixAPIMethod
+- rename ZabbixAPIObjectClass to ZabbixAPIObject
+- require >=python3.6
+
+### Reverts
+
+- chore: add more typings
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 1bfca8d..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1 +0,0 @@
-include README.markdown
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8e751c4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,51 @@
+all: lint test
+
+.DEFAULT_GOAL: install
+
+SHELL = bash
+CPU_CORES = $(shell N=$$(nproc); echo $$(( $$N > 4 ? 4 : $$N )))
+
+VENV = .venv
+$(VENV):
+	python3 -m venv $(VENV)
+	$(MAKE) install
+
+install: $(VENV)
+	$(VENV)/bin/pip install --upgrade pip setuptools wheel build
+	$(VENV)/bin/pip install --editable .[dev]
+
+format: $(VENV)
+	$(VENV)/bin/black .
+	$(VENV)/bin/isort .
+
+lint: $(VENV)
+	$(VENV)/bin/black . --check
+	$(VENV)/bin/isort . --check
+	$(VENV)/bin/pylint --jobs=$(CPU_CORES) --output-format=colorized pyzabbix tests
+	$(VENV)/bin/mypy pyzabbix tests
+
+
+PYTEST_CMD = $(VENV)/bin/pytest -v \
+		--numprocesses=$(CPU_CORES) \
+		--color=yes
+
+test: $(VENV)
+	$(PYTEST_CMD) \
+		--cov-config=./pyproject.toml \
+		--cov-report=term \
+		--cov-report=xml:./coverage.xml \
+		--cov=pyzabbix \
+		tests
+
+.PHONY: e2e
+e2e: $(VENV)
+	$(PYTEST_CMD) e2e
+
+build: $(VENV)
+	$(VENV)/bin/python -m build .
+
+release:
+	./scripts/release.sh
+
+clean:
+	rm -Rf $(VENV) dist
diff --git a/README.markdown b/README.md
similarity index 66%
rename from README.markdown
rename to README.md
index ad6a254..eff078d 100644
--- a/README.markdown
+++ b/README.md
@@ -1,14 +1,17 @@
-# PyZabbix #
+# PyZabbix
 
 **PyZabbix** is a Python module for working with the [Zabbix API](https://www.zabbix.com/documentation/current/manual/api/reference).
 
-[![Build Status](https://travis-ci.org/lukecyca/pyzabbix.png?branch=master)](https://travis-ci.org/lukecyca/pyzabbix)
-[![PyPi version](https://img.shields.io/pypi/v/pyzabbix.svg)](https://pypi.python.org/pypi/pyzabbix/)
+[![CI](https://github.com/lukecyca/pyzabbix/actions/workflows/ci.yml/badge.svg)](https://github.com/lukecyca/pyzabbix/actions/workflows/ci.yml)
+[![PyPI Package Version](https://img.shields.io/pypi/v/pyzabbix.svg)](https://pypi.org/project/pyzabbix/)
+[![PyPI Python Versions](https://img.shields.io/pypi/pyversions/pyzabbix.svg)](https://pypi.org/project/pyzabbix/)
 
 ## Requirements
-* Tested against Zabbix 1.8 through 5.0
 
-## Documentation ##
+- Tested against Zabbix 4.0 LTS, 5.0 LTS, 6.0 LTS and 6.4.
+
+## Documentation
+
 ### Getting Started
 
 Install PyZabbix using pip:
@@ -24,6 +27,8 @@ from pyzabbix import ZabbixAPI
 
 zapi = ZabbixAPI("http://zabbixserver.example.com")
 zapi.login("zabbix user", "zabbix pass")
+# You can also authenticate using an API token instead of user/pass with Zabbix >= 5.4
+# zapi.login(api_token='xxxxx')
 print("Connected to Zabbix API Version %s" % zapi.api_version())
 
 for h in zapi.host.get(output="extend"):
@@ -33,13 +38,15 @@ for h in zapi.host.get(output="extend"):
 Refer to the [Zabbix API Documentation](https://www.zabbix.com/documentation/current/manual/api/reference) and the [PyZabbix Examples](https://github.com/lukecyca/pyzabbix/tree/master/examples) for more information.
 
 ### Customizing the HTTP request
+
 PyZabbix uses the [requests](https://requests.readthedocs.io/en/master/) library for HTTP. You can customize the request parameters by configuring the [requests Session](https://requests.readthedocs.io/en/master/user/advanced/#session-objects) object used by PyZabbix.
 
 This is useful for:
-* Customizing headers
-* Enabling HTTP authentication
-* Enabling Keep-Alive
-* Disabling SSL certificate verification
+
+- Customizing headers
+- Enabling HTTP authentication
+- Enabling Keep-Alive
+- Disabling SSL certificate verification
 
 ```python
 from pyzabbix import ZabbixAPI
@@ -57,10 +64,15 @@ zapi.timeout = 5.1
 
 # Login (in case of HTTP Auth, only the username is needed, the password, if passed, will be ignored)
 zapi.login("http user", "http password")
+
+# You can also authenticate using an API token instead of user/pass with Zabbix >= 5.4
+# zapi.login(api_token='xxxxx')
 ```
 
 ### Enabling debug logging
+
 If you need to debug some issue with the Zabbix API, you can enable the output of logging, pyzabbix already uses the default python logging facility but by default, it logs to "Null", you can change this behavior on your program, here's an example:
+
 ```python
 import sys
 import logging
@@ -75,7 +87,12 @@ log.setLevel(logging.DEBUG)
 
 zapi = ZabbixAPI("http://zabbixserver.example.com")
 zapi.login('admin','password')
+
+# You can also authenticate using an API token instead of user/pass with Zabbix >= 5.4
+# zapi.login(api_token='xxxxx')
+
 ```
+
 The expected output is as following:
 
 ```
@@ -97,8 +114,34 @@ Response Body: {
 >>>
 ```
 
-## License ##
-LGPL 2.1   http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
+## Development
+
+To develop this project, start by reading the `Makefile` to have a basic understanding of the possible tasks.
+
+Install the project and the dependencies in a virtual environment:
+
+```sh
+make install
+source .venv/bin/activate
+```
+
+### Releases
+
+To release a new version, first bump the version number in `setup.py` by hand and run the release target:
+
+```sh
+make release
+```
+
+Finally, push the release commit and tag to publish them to Pypi:
+
+```sh
+git push --follow-tags
+```
+
+## License
+
+LGPL 2.1 http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 
 Zabbix API Python Library.
 
@@ -113,9 +156,9 @@ version 2.1 of the License, or (at your option) any later version.
 
 This library is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 Lesser General Public License for more details.
 
 You should have received a copy of the GNU Lesser General Public
 License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
diff --git a/debian/changelog b/debian/changelog
index 54a775f..3988ca0 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+pyzabbix (1.3.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 19 Jul 2023 16:31:43 -0000
+
 pyzabbix (0.8.2-1) unstable; urgency=medium
 
   [ Michal Arbet ]
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..fd7d4af
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,42 @@
+---
+# For development purpose only
+version: "3.8"
+
+services:
+  postgres-server:
+    image: postgres:13-alpine
+    environment:
+      POSTGRES_USER: zabbix
+      POSTGRES_PASSWORD: zabbix
+
+  zabbix-server:
+    image: zabbix/zabbix-server-pgsql:alpine-${ZABBIX_VERSION:-6.2}-latest
+    ports:
+      - 10051:10051
+    volumes:
+      - /etc/localtime:/etc/localtime:ro
+    environment:
+      POSTGRES_USER: zabbix
+      POSTGRES_PASSWORD: zabbix
+      ZBX_CACHEUPDATEFREQUENCY: 1
+    depends_on:
+      - postgres-server
+
+  zabbix-web:
+    image: zabbix/zabbix-web-nginx-pgsql:alpine-${ZABBIX_VERSION:-6.2}-latest
+    ports:
+      - 8888:8080
+    volumes:
+      - /etc/localtime:/etc/localtime:ro
+    environment:
+      POSTGRES_USER: zabbix
+      POSTGRES_PASSWORD: zabbix
+    depends_on:
+      - postgres-server
+      - zabbix-server
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:8080/"]
+      interval: 10s
+      timeout: 5s
+      retries: 3
+      start_period: 30s
diff --git a/e2e/__init__.py b/e2e/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/e2e/api_test.py b/e2e/api_test.py
new file mode 100644
index 0000000..a16a425
--- /dev/null
+++ b/e2e/api_test.py
@@ -0,0 +1,33 @@
+from pyzabbix import ZabbixAPI
+
+from .conftest import ZABBIX_VERSION
+
+
+def test_login(zapi: ZabbixAPI) -> None:
+    assert zapi.auth
+
+
+def test_version(zapi: ZabbixAPI) -> None:
+    assert zapi.api_version().startswith(ZABBIX_VERSION)
+
+
+def test_host_get(zapi: ZabbixAPI) -> None:
+    hosts = zapi.host.get(filter={"host": ["Zabbix server"]})
+    assert hosts[0]["host"] == "Zabbix server"
+
+
+def test_host_update_interface(zapi: ZabbixAPI) -> None:
+    hosts = zapi.host.get(filter={"host": ["Zabbix server"]}, output="extend")
+    assert hosts[0]["host"] == "Zabbix server"
+
+    interfaces = zapi.hostinterface.get(hostids=hosts[0]["hostid"])
+    assert interfaces[0]["ip"] == "127.0.0.1"
+
+    interfaces_update = zapi.hostinterface.update(
+        interfaceid=interfaces[0]["interfaceid"],
+        dns="zabbix-agent",
+    )
+    assert interfaces_update["interfaceids"] == [interfaces[0]["interfaceid"]]
+
+    interfaces = zapi.hostinterface.get(hostids=hosts[0]["hostid"])
+    assert interfaces[0]["dns"] == "zabbix-agent"
diff --git a/e2e/conftest.py b/e2e/conftest.py
new file mode 100644
index 0000000..8625f12
--- /dev/null
+++ b/e2e/conftest.py
@@ -0,0 +1,37 @@
+from os import getenv
+from time import sleep
+
+import pytest
+from requests.exceptions import ConnectionError
+
+from pyzabbix import ZabbixAPI, ZabbixAPIException
+
+ZABBIX_SERVER = "http://localhost:8888"
+ZABBIX_VERSION = getenv("ZABBIX_VERSION", "6.2")
+
+
+@pytest.fixture(scope="session", autouse=True)
+def wait_for_zabbix() -> None:
+    max_attempts = 30
+    while max_attempts > 0:
+        try:
+            ZabbixAPI(ZABBIX_SERVER).login("Admin", "zabbix")
+        except (ConnectionError, ZabbixAPIException):
+            sleep(2)
+            max_attempts -= 1
+            continue
+        break
+
+    if max_attempts <= 0:
+        pytest.exit("waiting for zabbix failed!", 1)
+
+    # extra sleep if zabbix wasn't ready on first attempt
+    if max_attempts < 30:
+        sleep(5)
+
+
+@pytest.fixture()
+def zapi() -> ZabbixAPI:
+    api = ZabbixAPI(ZABBIX_SERVER)
+    api.login("Admin", "zabbix")
+    return api
diff --git a/examples/add_item.py b/examples/add_item.py
index 4b6b3bf..fdc8b37 100644
--- a/examples/add_item.py
+++ b/examples/add_item.py
@@ -2,37 +2,38 @@
 Looks up a host based on its name, and then adds an item to it
 """
 
-from pyzabbix import ZabbixAPI, ZabbixAPIException
 import sys
 
+from pyzabbix import ZabbixAPI, ZabbixAPIException
+
 # The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.com"
 
 zapi = ZabbixAPI(ZABBIX_SERVER)
 
 # Login to the Zabbix API
-zapi.login('Admin', 'zabbix')
+zapi.login("Admin", "zabbix")
 
-host_name = 'example.com'
+host_name = "example.com"
 
 hosts = zapi.host.get(filter={"host": host_name}, selectInterfaces=["interfaceid"])
 if hosts:
     host_id = hosts[0]["hostid"]
-    print("Found host id {0}".format(host_id))
+    print(f"Found host id {host_id}")
 
     try:
         item = zapi.item.create(
             hostid=host_id,
-            name='Used disk space on $1 in %',
-            key_='vfs.fs.size[/,pused]',
+            name="Used disk space on $1 in %",
+            key_="vfs.fs.size[/,pused]",
             type=0,
             value_type=3,
             interfaceid=hosts[0]["interfaces"][0]["interfaceid"],
-            delay=30
+            delay=30,
         )
     except ZabbixAPIException as e:
         print(e)
         sys.exit()
-    print("Added item with itemid {0} to host: {1}".format(item["itemids"][0], host_name))
+    print("Added item with itemid {} to host: {}".format(item["itemids"][0], host_name))
 else:
     print("No hosts found")
diff --git a/examples/additemcsv.py b/examples/additemcsv.py
new file mode 100644
index 0000000..5e024c8
--- /dev/null
+++ b/examples/additemcsv.py
@@ -0,0 +1,81 @@
+"""
+Desenvolvimento: WagPTech
+
+Programa para incluir Item no Zabbix em um Host específico.
+
+Sintaxe: python ZBXadditem.py host_name arquivo.csv
+
+Entrar com login e senha do zabbix (necessário ser ADM)
+
+Formato do arquivo:
+key
+item1
+item2
+...
+"""
+
+import csv
+import getpass
+import os
+import sys
+
+from pyzabbix import ZabbixAPI, ZabbixAPIException
+
+host_name = sys.argv[1]
+arquivo = sys.argv[2]
+open(arquivo, newline="", encoding="utf-8")
+
+# Zabbix server
+
+zapi = ZabbixAPI("http://30.0.0.47/zabbix")
+
+# Login com o Zabbix API
+
+login = input("Insira seu login: ")
+passwd = getpass.getpass("Digite sua senha: ")
+
+zapi.login(login, passwd)
+
+add = int(0)
+nadd = int(0)
+
+hosts = zapi.host.get(filter={"host": host_name}, selectInterfaces=["interfaceid"])
+if hosts:
+    host_id = hosts[0]["hostid"]
+    print("host_name " + host_name + f" @ host id {host_id}")
+    with open(
+        arquivo, newline="", encoding="utf-8"
+    ) as csvfile:  # sys.argv[2]/'zbx_l15_k10.csv'
+        reader = csv.DictReader(csvfile)
+        for row in reader:
+            try:
+                # print ("Add Item: " + row['key'])
+                item = zapi.item.create(
+                    hostid=host_id,
+                    name=row["key"],
+                    key_=row["key"],
+                    type=2,  # 0-Zabbix agent; 2-Zabbix trapper; 3-simple check; 5-Zabbix internal;
+                    # 7-Zabbix agent (active); 8-Zabbix aggregate; 9-web item;
+                    # 10-external check; 11-database monitor; 12-IPMI agent;
+                    # 13-SSH agent; 14-TELNET agent; 15-calculated;
+                    # 16-JMX agent; 17-SNMP trap; 18-Dependent item; 19-HTTP agent; 20-SNMP agent; 21-Script.
+                    value_type=3,  # 0-numeric float; 1-character; 2-log; 3-numeric unsigned; 4-text.
+                    interfaceid=hosts[0]["interfaces"][0]["interfaceid"],
+                    delay=60,
+                    status=1,  # 0-enabled item; 1-disabled item.
+                )
+                add = add + 1
+                if add > 0:
+                    print("Adicionados: " + str(add))
+            except ZabbixAPIException as error:
+                nadd = nadd + 1
+                if nadd > 0:
+                    print("Recusados: " + str(nadd))
+                # print(error)
+        if add > 0:
+            print("Total de itens adicionados: " + str(add) + " itens.")
+        if nadd > 0:
+            print("Total de itens recusados: " + str(nadd) + " itens.")
+        sys.exit()
+else:
+    print("No hosts found")
diff --git a/examples/current_issues.py b/examples/current_issues.py
index 5798f57..3e3f104 100644
--- a/examples/current_issues.py
+++ b/examples/current_issues.py
@@ -5,42 +5,46 @@ Shows a list of all current issues (AKA tripped triggers)
 from pyzabbix import ZabbixAPI
 
 # The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.com"
 
 zapi = ZabbixAPI(ZABBIX_SERVER)
 
 # Login to the Zabbix API
-zapi.login('api_username', 'api_password')
+zapi.login("api_username", "api_password")
 
 # Get a list of all issues (AKA tripped triggers)
-triggers = zapi.trigger.get(only_true=1,
-                            skipDependent=1,
-                            monitored=1,
-                            active=1,
-                            output='extend',
-                            expandDescription=1,
-                            selectHosts=['host'],
-                            )
+triggers = zapi.trigger.get(
+    only_true=1,
+    skipDependent=1,
+    monitored=1,
+    active=1,
+    output="extend",
+    expandDescription=1,
+    selectHosts=["host"],
+)
 
 # Do another query to find out which issues are Unacknowledged
-unack_triggers = zapi.trigger.get(only_true=1,
-                                  skipDependent=1,
-                                  monitored=1,
-                                  active=1,
-                                  output='extend',
-                                  expandDescription=1,
-                                  selectHosts=['host'],
-                                  withLastEventUnacknowledged=1,
-                                  )
-unack_trigger_ids = [t['triggerid'] for t in unack_triggers]
+unack_triggers = zapi.trigger.get(
+    only_true=1,
+    skipDependent=1,
+    monitored=1,
+    active=1,
+    output="extend",
+    expandDescription=1,
+    selectHosts=["host"],
+    withLastEventUnacknowledged=1,
+)
+unack_trigger_ids = [t["triggerid"] for t in unack_triggers]
 for t in triggers:
-    t['unacknowledged'] = True if t['triggerid'] in unack_trigger_ids \
-        else False
+    t["unacknowledged"] = True if t["triggerid"] in unack_trigger_ids else False
 
 # Print a list containing only "tripped" triggers
 for t in triggers:
-    if int(t['value']) == 1:
-        print("{0} - {1} {2}".format(t['hosts'][0]['host'],
-                                     t['description'],
-                                     '(Unack)' if t['unacknowledged'] else '')
-              )
+    if int(t["value"]) == 1:
+        print(
+            "{} - {} {}".format(
+                t["hosts"][0]["host"],
+                t["description"],
+                "(Unack)" if t["unacknowledged"] else "",
+            )
+        )
diff --git a/examples/export_history_csv.py b/examples/export_history_csv.py
new file mode 100644
index 0000000..564d066
--- /dev/null
+++ b/examples/export_history_csv.py
@@ -0,0 +1,196 @@
+# The research leading to these results has received funding from the
+# European Commission's Seventh Framework Programme (FP7/2007-13)
+# under grant agreement no 257386.
+# 	http://www.bonfire-project.eu/
+# Copyright 2012 Yahya Al-Hazmi, TU Berlin
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# 	http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+import argparse
+import datetime
+import sys
+import time
+
+from pyzabbix import ZabbixAPI
+
+
+def login(zapi, username, password):
+    try:
+        zapi.login(username, password)
+        print("login succeed.")
+    except:
+        print("zabbix server is not reachable: ")
+        sys.exit()
+
+
+def getHostId(zapi, hostname, server):
+    if hostname == "":
+        print("hostname is missed")
+        sys.exit()
+    host = zapi.host.get(filter={"host": hostname}, output="extend")
+    if len(host) == 0:
+        print(f"hostname: {hostname} not found in zabbix server: {server}, exit")
+        sys.exit()
+    else:
+        return host[0]["hostid"]
+
+
+def getItems(zapi, key, hostid, hostname):
+    items = zapi.item.get(search={"key_": key}, hostids=hostid, output="extend")
+    if len(items) == 0:
+        print(f"item key: {key} not found in hostname: {hostname}")
+        sys.exit()
+    else:
+        return items
+
+
+def convertTimeStamp(inputTime):
+    if inputTime == "":
+        return ""
+    try:
+        tmpDate = datetime.datetime.strptime(inputTime, "%Y-%m-%d %H:%M:%S")
+        timestamp = int(time.mktime(tmpDate.timetuple()))
+    except:
+        print("time data %s does not match format Y-m-d H:M:S, exit" % (datetime))
+        sys.exit()
+
+    return timestamp
+
+
+def generateOutputFilename(output, hostname, key):
+    if output == "":
+        return hostname + "_" + key + ".csv"
+    else:
+        return output
+
+
+def exportToCSV(historys, key, output):
+    f = open(output, "w")
+    inc = 0
+    f.write("key;timestamp;valuei\n")  # csv header
+    for history in historys:
+        f.write("{};{};{}\n".format(key, history["clock"], history["value"]))
+        inc = inc + 1
+    print("exported %i history to %s" % (inc, output))
+    f.close()
+
+
+def assignTimeRange(inputParameters, datetime1, datetime2):
+    timestamp1 = convertTimeStamp(datetime1)
+    timestamp2 = convertTimeStamp(datetime2)
+
+    # only timestamp1
+    if timestamp1 and not timestamp2:
+        inputParameters["time_from"] = timestamp1
+        inputParameters["time_till"] = convertTimeStamp(time.time())  # current time
+    # only timestamp2
+    elif not timestamp1 and timestamp2:
+        inputParameters["time_from"] = timestamp2
+        inputParameters["time_till"] = timestamp2
+    # no inserted both timestamps
+    elif not timestamp1 and not timestamp2:
+        inputParameters["time_from"] = convertTimeStamp(time.time())  # current time
+        inputParameters["time_till"] = convertTimeStamp(time.time())  # current time
+    # inserted both timestamps
+    else:
+        inputParameters["time_from"] = timestamp1
+        inputParameters["time_till"] = timestamp2
+
+
+def fetch_to_csv(
+    username, password, server, hostname, key, output, datetime1, datetime2, debuglevel
+):
+    print("login to zabbix server %s" % server)
+    zapi = ZabbixAPI(server + "/zabbix")
+    login(zapi, username, password)
+    hostid = getHostId(zapi, hostname, server)
+
+    # find itemid using key
+    print("key is: %s" % (key))
+    items = getItems(zapi, key, hostid, hostname)
+    item = items[0]
+
+    # parameter validation
+    inputParameters = {}
+    inputParameters["history"] = item["value_type"]
+    inputParameters["output"] = "extend"
+    inputParameters["itemids"] = [item["itemid"]]
+
+    assignTimeRange(inputParameters, datetime1, datetime2)
+
+    # get history
+    print("get history using this parameter")
+    print(inputParameters)
+    history = zapi.history.get(**inputParameters)
+
+    # export to File
+    output = generateOutputFilename(output, hostname, key)
+    exportToCSV(history, key, output)
+
+
+# Parsing Parameters
+parser = argparse.ArgumentParser(
+    description="Fetch history from aggregator and save it into CSV file"
+)
+parser.add_argument("-s", dest="server_IP", required=True, help="aggregator IP address")
+parser.add_argument(
+    "-n", dest="hostname", required=True, help="name of the monitored host"
+)
+parser.add_argument(
+    "-u",
+    dest="username",
+    default="Admin",
+    required=True,
+    help="zabbix username, default Admin",
+)
+parser.add_argument(
+    "-p", dest="password", default="zabbix", required=True, help="zabbix password"
+)
+parser.add_argument(
+    "-k",
+    dest="key",
+    default="",
+    required=True,
+    help="zabbix item key, if not specified the script will fetch all keys for the specified hostname",
+)
+parser.add_argument(
+    "-o", dest="output", default="", help="output file name, default hostname.csv"
+)
+parser.add_argument(
+    "-t1",
+    dest="datetime1",
+    default="",
+    help="begin date-time, use this pattern '2011-11-08 14:49:43' if only t1 specified then time period will be t1-now ",
+)
+parser.add_argument(
+    "-t2",
+    dest="datetime2",
+    default="",
+    help="end date-time, use this pattern '2011-11-08 14:49:43'",
+)
+parser.add_argument(
+    "-v", dest="debuglevel", default=0, type=int, help="log level, default 0"
+)
+args = parser.parse_args()
+
+# Calling fetching function
+fetch_to_csv(
+    args.username,
+    args.password,
+    args.server_IP,
+    args.hostname,
+    args.key,
+    args.output,
+    args.datetime1,
+    args.datetime2,
+    args.debuglevel,
+)
diff --git a/examples/fix_host_ips.py b/examples/fix_host_ips.py
index cbc5220..17f1b0c 100644
--- a/examples/fix_host_ips.py
+++ b/examples/fix_host_ips.py
@@ -7,10 +7,11 @@ and fixes it if required.
 """
 
 import socket
+
 from pyzabbix import ZabbixAPI, ZabbixAPIException
 
 # The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.com"
 
 zapi = ZabbixAPI(ZABBIX_SERVER)
 
@@ -18,37 +19,40 @@ zapi = ZabbixAPI(ZABBIX_SERVER)
 zapi.session.verify = False
 
 # Login to the Zabbix API
-zapi.login('Admin', 'zabbix')
+zapi.login("Admin", "zabbix")
 
 # Loop through all hosts interfaces, getting only "main" interfaces of type "agent"
-for h in zapi.hostinterface.get(output=["dns", "ip", "useip"], selectHosts=["host"], filter={"main": 1, "type": 1}):
+for h in zapi.hostinterface.get(
+    output=["dns", "ip", "useip"], selectHosts=["host"], filter={"main": 1, "type": 1}
+):
     # Make sure the hosts are named according to their FQDN
-    if h['dns'] != h['hosts'][0]['host']:
-        print('Warning: %s has dns "%s"' % (h['hosts'][0]['host'], h['dns']))
+    if h["dns"] != h["hosts"][0]["host"]:
+        print('Warning: {} has dns "{}"'.format(h["hosts"][0]["host"], h["dns"]))
 
     # Make sure they are using hostnames to connect rather than IPs (could be also filtered in the get request)
-    if h['useip'] == '1':
-        print('%s is using IP instead of hostname. Skipping.' % h['hosts'][0]['host'])
+    if h["useip"] == "1":
+        print("%s is using IP instead of hostname. Skipping." % h["hosts"][0]["host"])
         continue
 
     # Do a DNS lookup for the host's DNS name
     try:
-        lookup = socket.gethostbyaddr(h['dns'])
+        lookup = socket.gethostbyaddr(h["dns"])
     except socket.gaierror as e:
-        print(h['dns'], e)
+        print(h["dns"], e)
         continue
     actual_ip = lookup[2][0]
 
     # Check whether the looked-up IP matches the one stored in the host's IP
     # field
-    if actual_ip != h['ip']:
-        print("%s has the wrong IP: %s. Changing it to: %s" % (h['hosts'][0]['host'],
-                                                               h['ip'],
-                                                               actual_ip))
+    if actual_ip != h["ip"]:
+        print(
+            "%s has the wrong IP: %s. Changing it to: %s"
+            % (h["hosts"][0]["host"], h["ip"], actual_ip)
+        )
 
         # Set the host's IP field to match what the DNS lookup said it should
         # be
         try:
-            zapi.hostinterface.update(interfaceid=h['interfaceid'], ip=actual_ip)
+            zapi.hostinterface.update(interfaceid=h["interfaceid"], ip=actual_ip)
         except ZabbixAPIException as e:
             print(e)
diff --git a/examples/history_data.py b/examples/history_data.py
index b4f6cac..b0b2e44 100644
--- a/examples/history_data.py
+++ b/examples/history_data.py
@@ -2,43 +2,50 @@
 Retrieves history data for a given numeric (either int or float) item_id
 """
 
-from pyzabbix import ZabbixAPI
-from datetime import datetime
 import time
+from datetime import datetime
+
+from pyzabbix import ZabbixAPI
 
 # The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'http://localhost/zabbix'
+ZABBIX_SERVER = "http://localhost/zabbix"
 
 zapi = ZabbixAPI(ZABBIX_SERVER)
 
 # Login to the Zabbix API
-zapi.login('Admin', 'zabbix')
+zapi.login("Admin", "zabbix")
 
-item_id = 'item_id'
+item_id = "item_id"
 
 # Create a time range
 time_till = time.mktime(datetime.now().timetuple())
 time_from = time_till - 60 * 60 * 4  # 4 hours
 
 # Query item's history (integer) data
-history = zapi.history.get(itemids=[item_id],
-                           time_from=time_from,
-                           time_till=time_till,
-                           output='extend',
-                           limit='5000',
-                           )
+history = zapi.history.get(
+    itemids=[item_id],
+    time_from=time_from,
+    time_till=time_till,
+    output="extend",
+    limit="5000",
+)
 
 # If nothing was found, try getting it from history (float) data
 if not len(history):
-    history = zapi.history.get(itemids=[item_id],
-                               time_from=time_from,
-                               time_till=time_till,
-                               output='extend',
-                               limit='5000',
-                               history=0,
-                               )
+    history = zapi.history.get(
+        itemids=[item_id],
+        time_from=time_from,
+        time_till=time_till,
+        output="extend",
+        limit="5000",
+        history=0,
+    )
 
 # Print out each datapoint
 for point in history:
-    print("{0}: {1}".format(datetime.fromtimestamp(int(point['clock']))
-                            .strftime("%x %X"), point['value']))
+    print(
+        "{}: {}".format(
+            datetime.fromtimestamp(int(point["clock"])).strftime("%x %X"),
+            point["value"],
+        )
+    )
diff --git a/examples/import_templates.py b/examples/import_templates.py
index 5f31600..9cf6022 100644
--- a/examples/import_templates.py
+++ b/examples/import_templates.py
@@ -1,102 +1,68 @@
-"""                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        
+"""
 Import Zabbix XML templates
-"""                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        
-                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
-from pyzabbix import ZabbixAPI, ZabbixAPIException
+"""
+
 import os
-import sys                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
+import sys
+
+from pyzabbix import ZabbixAPI, ZabbixAPIException
 
 if len(sys.argv) <= 1:
-    print('Please provide directory with templates as first ARG or the XML file with template.')
+    print(
+        "Please provide directory with templates as first ARG or the XML file with template."
+    )
     exit(1)
 
 path = sys.argv[1]
 
 # The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.org'
+ZABBIX_SERVER = "https://zabbix.example.org"
 
 zapi = ZabbixAPI(ZABBIX_SERVER)
 
 # Login to the Zabbix API
-#zapi.session.verify = False
+# zapi.session.verify = False
 zapi.login("Admin", "zabbix")
 
 rules = {
-    'applications': {
-        'createMissing': True,
-    },
-    'discoveryRules': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'graphs': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'groups': {
-        'createMissing': True
-    },
-    'hosts': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'images': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'items': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'maps': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'screens': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'templateLinkage': {
-        'createMissing': True,
-    },
-    'templates': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'templateScreens': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'triggers': {
-        'createMissing': True,
-        'updateExisting': True
-    },
-    'valueMaps': {
-        'createMissing': True,
-        'updateExisting': True
+    "applications": {
+        "createMissing": True,
     },
+    "discoveryRules": {"createMissing": True, "updateExisting": True},
+    "graphs": {"createMissing": True, "updateExisting": True},
+    "groups": {"createMissing": True},
+    "hosts": {"createMissing": True, "updateExisting": True},
+    "images": {"createMissing": True, "updateExisting": True},
+    "items": {"createMissing": True, "updateExisting": True},
+    "maps": {"createMissing": True, "updateExisting": True},
+    "screens": {"createMissing": True, "updateExisting": True},
+    "templateLinkage": {"createMissing": True},
+    "templates": {"createMissing": True, "updateExisting": True},
+    "templateScreens": {"createMissing": True, "updateExisting": True},
+    "triggers": {"createMissing": True, "updateExisting": True},
+    "valueMaps": {"createMissing": True, "updateExisting": True},
 }
 
 if os.path.isdir(path):
-    #path = path/*.xml
-    files = glob.glob(path+'/*.xml')
+    # path = path/*.xml
+    files = glob.glob(path + "/*.xml")
     for file in files:
         print(file)
-        with open(file, 'r') as f:
+        with open(file) as f:
             template = f.read()
             try:
-                zapi.confimport('xml', template, rules)
+                zapi.confimport("xml", template, rules)
             except ZabbixAPIException as e:
                 print(e)
-        print('')
+        print("")
 elif os.path.isfile(path):
     files = glob.glob(path)
     for file in files:
-        with open(file, 'r') as f:
+        with open(file) as f:
             template = f.read()
             try:
-                zapi.confimport('xml', template, rules)
+                zapi.confimport("xml", template, rules)
             except ZabbixAPIException as e:
                 print(e)
 else:
-    print('I need a xml file')
+    print("I need a xml file")
diff --git a/examples/timeout.py b/examples/timeout.py
index 7b30b6b..58617ab 100644
--- a/examples/timeout.py
+++ b/examples/timeout.py
@@ -5,7 +5,7 @@ Set connect and read timeout for ZabbixAPI requests
 from pyzabbix import ZabbixAPI
 
 # The hostname at which the Zabbix web interface is available
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.com"
 
 # Timeout (float) in seconds
 # By default this timeout affects both the "connect" and "read", but
@@ -17,7 +17,7 @@ TIMEOUT = 3.5
 zapi = ZabbixAPI(ZABBIX_SERVER, timeout=TIMEOUT)
 
 # Login to the Zabbix API
-zapi.login('api_username', 'api_password')
+zapi.login("api_username", "api_password")
 
 # Or you can re-define it after
 zapi.timeout = TIMEOUT
diff --git a/examples/with_context.py b/examples/with_context.py
index bebd936..97f4fde 100644
--- a/examples/with_context.py
+++ b/examples/with_context.py
@@ -4,11 +4,11 @@ Prints hostnames for all known hosts.
 
 from pyzabbix import ZabbixAPI
 
-ZABBIX_SERVER = 'https://zabbix.example.com'
+ZABBIX_SERVER = "https://zabbix.example.com"
 
 # Use context manager to auto-logout after request is done.
 with ZabbixAPI(ZABBIX_SERVER) as zapi:
-    zapi.login('api_username', 'api_password')
-    hosts = zapi.host.get(output=['name'])
+    zapi.login("api_username", "api_password")
+    hosts = zapi.host.get(output=["name"])
     for host in hosts:
-        print(host['name'])
+        print(host["name"])
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..b8ec283
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,28 @@
+[tool.isort]
+profile = "black"
+combine_as_imports = true
+
+[tool.pylint.messages_control]
+disable = [
+  "missing-class-docstring",
+  "missing-function-docstring",
+  "missing-module-docstring",
+]
+
+[tool.pylint.format]
+disable = "logging-fstring-interpolation"
+
+[tool.mypy]
+allow_redefinition = true
+disallow_incomplete_defs = true
+
+[tool.pytest.ini_options]
+log_cli = true
+log_cli_level = "DEBUG"
+
+[tool.coverage.run]
+omit = ["tests/*", "e2e/*"]
+
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/pyzabbix/__init__.py b/pyzabbix/__init__.py
index 86112e4..5603ad6 100644
--- a/pyzabbix/__init__.py
+++ b/pyzabbix/__init__.py
@@ -1,195 +1,7 @@
-import logging
-import requests
-import json
-
-
-class _NullHandler(logging.Handler):
-    def emit(self, record):
-        pass
-
-logger = logging.getLogger(__name__)
-logger.addHandler(_NullHandler())
-
-
-class ZabbixAPIException(Exception):
-    """ generic zabbix api exception
-    code list:
-         -32700 - invalid JSON. An error occurred on the server while parsing the JSON text (typo, wrong quotes, etc.)
-         -32600 - received JSON is not a valid JSON-RPC Request 
-         -32601 - requested remote-procedure does not exist
-         -32602 - invalid method parameters
-         -32603 - Internal JSON-RPC error
-         -32400 - System error
-         -32300 - Transport error
-         -32500 - Application error
-    """
-    def __init__(self, *args, **kwargs):
-        super(ZabbixAPIException, self).__init__(*args)
-
-        self.error = kwargs.get("error", None)
-
-
-class ZabbixAPI(object):
-    def __init__(self,
-                 server='http://localhost/zabbix',
-                 session=None,
-                 use_authenticate=False,
-                 timeout=None):
-        """
-        Parameters:
-            server: Base URI for zabbix web interface (omitting /api_jsonrpc.php)
-            session: optional pre-configured requests.Session instance
-            use_authenticate: Use old (Zabbix 1.8) style authentication
-            timeout: optional connect and read timeout in seconds, default: None (if you're using Requests >= 2.4 you can set it as tuple: "(connect, read)" which is used to set individual connect and read timeouts.)
-        """
-
-        if session:
-            self.session = session
-        else:
-            self.session = requests.Session()
-
-        # Default headers for all requests
-        self.session.headers.update({
-            'Content-Type': 'application/json-rpc',
-            'User-Agent': 'python/pyzabbix',
-            'Cache-Control': 'no-cache'
-        })
-
-        self.use_authenticate = use_authenticate
-        self.auth = ''
-        self.id = 0
-
-        self.timeout = timeout
-
-        self.url = server + '/api_jsonrpc.php' if not server.endswith('/api_jsonrpc.php') else server
-        logger.info("JSON-RPC Server Endpoint: %s", self.url)
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exception_type, exception_value, traceback):
-        if isinstance(exception_value, (ZabbixAPIException, type(None))):
-            if self.is_authenticated:
-                self.user.logout()
-            return True
-
-    def login(self, user='', password=''):
-        """Convenience method for calling user.authenticate and storing the resulting auth token
-           for further commands.
-           If use_authenticate is set, it uses the older (Zabbix 1.8) authentication command
-           :param password: Password used to login into Zabbix
-           :param user: Username used to login into Zabbix
-        """
-
-        # If we have an invalid auth token, we are not allowed to send a login
-        # request. Clear it before trying.
-        self.auth = ''
-        if self.use_authenticate:
-            self.auth = self.user.authenticate(user=user, password=password)
-        else:
-            self.auth = self.user.login(user=user, password=password)
-
-    def check_authentication(self):
-        """Convenience method for calling user.checkAuthentication of the current session"""
-        return self.user.checkAuthentication(sessionid=self.auth)
-
-    @property
-    def is_authenticated(self):
-        try:
-            self.user.checkAuthentication(sessionid=self.auth)
-        except ZabbixAPIException:
-            return False
-        return True
-
-    def confimport(self, confformat='', source='', rules=''):
-        """Alias for configuration.import because it clashes with
-           Python's import reserved keyword
-           :param rules:
-           :param source:
-           :param confformat:
-        """
-
-        return self.do_request(
-            method="configuration.import",
-            params={"format": confformat, "source": source, "rules": rules}
-        )['result']
-
-    def api_version(self):
-        return self.apiinfo.version()
-
-    def do_request(self, method, params=None):
-        request_json = {
-            'jsonrpc': '2.0',
-            'method': method,
-            'params': params or {},
-            'id': self.id,
-        }
-
-        # We don't have to pass the auth token if asking for the apiinfo.version or user.checkAuthentication
-        if self.auth and method != 'apiinfo.version' and method != 'user.checkAuthentication':
-            request_json['auth'] = self.auth
-
-        logger.debug("Sending: %s", json.dumps(request_json,
-                                               indent=4,
-                                               separators=(',', ': ')))
-        response = self.session.post(
-            self.url,
-            data=json.dumps(request_json),
-            timeout=self.timeout
-        )
-        logger.debug("Response Code: %s", str(response.status_code))
-
-        # NOTE: Getting a 412 response code means the headers are not in the
-        # list of allowed headers.
-        response.raise_for_status()
-
-        if not len(response.text):
-            raise ZabbixAPIException("Received empty response")
-
-        try:
-            response_json = json.loads(response.text)
-        except ValueError:
-            raise ZabbixAPIException(
-                "Unable to parse json: %s" % response.text
-            )
-        logger.debug("Response Body: %s", json.dumps(response_json,
-                                                     indent=4,
-                                                     separators=(',', ': ')))
-
-        self.id += 1
-
-        if 'error' in response_json:  # some exception
-            if 'data' not in response_json['error']:  # some errors don't contain 'data': workaround for ZBX-9340
-                response_json['error']['data'] = "No data"
-            msg = u"Error {code}: {message}, {data}".format(
-                code=response_json['error']['code'],
-                message=response_json['error']['message'],
-                data=response_json['error']['data']
-            )
-            raise ZabbixAPIException(msg, response_json['error']['code'], error=response_json['error'])
-
-        return response_json
-
-    def __getattr__(self, attr):
-        """Dynamically create an object class (ie: host)"""
-        return ZabbixAPIObjectClass(attr, self)
-
-
-class ZabbixAPIObjectClass(object):
-    def __init__(self, name, parent):
-        self.name = name
-        self.parent = parent
-
-    def __getattr__(self, attr):
-        """Dynamically create a method (ie: get)"""
-
-        def fn(*args, **kwargs):
-            if args and kwargs:
-                raise TypeError("Found both args and kwargs")
-
-            return self.parent.do_request(
-                '{0}.{1}'.format(self.name, attr),
-                args or kwargs
-            )['result']
-
-        return fn
+from .api import (
+    ZabbixAPI,
+    ZabbixAPIException,
+    ZabbixAPIMethod,
+    ZabbixAPIObject,
+    ZabbixAPIObjectClass,
+)
diff --git a/pyzabbix/api.py b/pyzabbix/api.py
new file mode 100644
index 0000000..d3d2fe1
--- /dev/null
+++ b/pyzabbix/api.py
@@ -0,0 +1,305 @@
+# pylint: disable=wrong-import-order
+
+import logging
+from typing import Mapping, Optional, Sequence, Tuple, Union
+from warnings import warn
+
+from packaging.version import Version
+from requests import Session
+
+__all__ = [
+    "ZabbixAPI",
+    "ZabbixAPIException",
+    "ZabbixAPIMethod",
+    "ZabbixAPIObject",
+    "ZabbixAPIObjectClass",
+]
+
+logger = logging.getLogger(__name__)
+logger.addHandler(logging.NullHandler())
+
+ZABBIX_5_4_0 = Version("5.4.0")
+ZABBIX_6_4_0 = Version("6.4.0")
+
+
+class ZabbixAPIException(Exception):
+    """Generic Zabbix API exception
+
+    Codes:
+      -32700: invalid JSON. An error occurred on the server while
+              parsing the JSON text (typo, wrong quotes, etc.)
+      -32600: received JSON is not a valid JSON-RPC Request
+      -32601: requested remote-procedure does not exist
+      -32602: invalid method parameters
+      -32603: Internal JSON-RPC error
+      -32400: System error
+      -32300: Transport error
+      -32500: Application error
+    """
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args)
+
+        self.error = kwargs.get("error", None)
+
+
+# pylint: disable=too-many-instance-attributes
+class ZabbixAPI:
+    # pylint: disable=too-many-arguments
+    def __init__(
+        self,
+        server: str = "http://localhost/zabbix",
+        session: Optional[Session] = None,
+        use_authenticate: bool = False,
+        timeout: Optional[Union[float, int, Tuple[int, int]]] = None,
+        detect_version: bool = True,
+    ):
+        """
+        :param server: Base URI for zabbix web interface (omitting /api_jsonrpc.php)
+        :param session: optional pre-configured requests.Session instance
+        :param use_authenticate: Use old (Zabbix 1.8) style authentication
+        :param timeout: optional connect and read timeout in seconds, default: None
+                        If you're using Requests >= 2.4 you can set it as
+                        tuple: "(connect, read)" which is used to set individual
+                        connect and read timeouts.
+        :param detect_version: autodetect Zabbix API version
+        """
+        self.session = session or Session()
+
+        # Default headers for all requests
+        self.session.headers.update(
+            {
+                "Content-Type": "application/json-rpc",
+                "User-Agent": "python/pyzabbix",
+                "Cache-Control": "no-cache",
+            }
+        )
+
+        self.use_authenticate = use_authenticate
+        self.use_api_token = False
+        self.auth = ""
+        self.id = 0  # pylint: disable=invalid-name
+
+        self.timeout = timeout
+
+        if not server.endswith("/api_jsonrpc.php"):
+            server = server.rstrip("/") + "/api_jsonrpc.php"
+        self.url = server
+        logger.info(f"JSON-RPC Server Endpoint: {self.url}")
+
+        self.version: Optional[Version] = None
+        self._detect_version = detect_version
+
+    def __enter__(self) -> "ZabbixAPI":
+        return self
+
+    # pylint: disable=inconsistent-return-statements
+    def __exit__(self, exception_type, exception_value, traceback):
+        if isinstance(exception_value, (ZabbixAPIException, type(None))):
+            if self.is_authenticated and not self.use_api_token:
+                # Logout the user if they are authenticated using username + password.
+                self.user.logout()
+            return True
+        return None
+
+    def login(
+        self,
+        user: str = "",
+        password: str = "",
+        api_token: Optional[str] = None,
+    ) -> None:
+        """Convenience method for calling user.authenticate
+        and storing the resulting auth token for further commands.
+
+        If use_authenticate is set, it uses the older (Zabbix 1.8)
+        authentication command
+
+        :param password: Password used to login into Zabbix
+        :param user: Username used to login into Zabbix
+        :param api_token: API Token to authenticate with
+        """
+
+        if self._detect_version:
+            self.version = Version(self.api_version())
+            logger.info(f"Zabbix API version is: {self.version}")
+
+        # If the API token is explicitly provided, use this instead.
+        if api_token is not None:
+            self.use_api_token = True
+            self.auth = api_token
+            return
+
+        # If we have an invalid auth token, we are not allowed to send a login
+        # request. Clear it before trying.
+        self.auth = ""
+        if self.use_authenticate:
+            self.auth = self.user.authenticate(user=user, password=password)
+        elif self.version and self.version >= ZABBIX_5_4_0:
+            self.auth = self.user.login(username=user, password=password)
+        else:
+            self.auth = self.user.login(user=user, password=password)
+
+    def check_authentication(self):
+        if self.use_api_token:
+            # We cannot use this call using an API Token
+            return True
+        # Convenience method for calling user.checkAuthentication of the current session
+        return self.user.checkAuthentication(sessionid=self.auth)
+
+    @property
+    def is_authenticated(self) -> bool:
+        if self.use_api_token:
+            # We cannot use this call using an API Token
+            return True
+
+        try:
+            self.user.checkAuthentication(sessionid=self.auth)
+        except ZabbixAPIException:
+            return False
+        return True
+
+    def confimport(
+        self,
+        confformat: str = "",
+        source: str = "",
+        rules: str = "",
+    ) -> dict:
+        """Alias for configuration.import because it clashes with
+        Python's import reserved keyword
+        :param rules:
+        :param source:
+        :param confformat:
+        """
+        warn(
+            "ZabbixAPI.confimport(format, source, rules) has been deprecated, please use "
+            "ZabbixAPI.configuration['import'](format=format, source=source, rules=rules) instead",
+            DeprecationWarning,
+            2,
+        )
+
+        return self.configuration["import"](
+            format=confformat,
+            source=source,
+            rules=rules,
+        )
+
+    def api_version(self) -> str:
+        return self.apiinfo.version()
+
+    def do_request(
+        self,
+        method: str,
+        params: Optional[Union[Mapping, Sequence]] = None,
+    ) -> dict:
+        payload = {
+            "jsonrpc": "2.0",
+            "method": method,
+            "params": params or {},
+            "id": self.id,
+        }
+        headers = {}
+
+        # We don't have to pass the auth token if asking for
+        # the apiinfo.version or user.checkAuthentication
+        anonymous_methods = {
+            "apiinfo.version",
+            "user.checkAuthentication",
+            "user.login",
+        }
+        if self.auth and method not in anonymous_methods:
+            if self.version and self.version >= ZABBIX_6_4_0:
+                headers["Authorization"] = f"Bearer {self.auth}"
+            else:
+                payload["auth"] = self.auth
+
+        logger.debug(f"Sending: {payload}")
+        resp = self.session.post(
+            self.url,
+            json=payload,
+            headers=headers,
+            timeout=self.timeout,
+        )
+        logger.debug(f"Response Code: {resp.status_code}")
+
+        # NOTE: Getting a 412 response code means the headers are not in the
+        # list of allowed headers.
+        resp.raise_for_status()
+
+        if not resp.text:
+            raise ZabbixAPIException("Received empty response")
+
+        try:
+            response = resp.json()
+        except ValueError as exception:
+            raise ZabbixAPIException(
+                f"Unable to parse json: {resp.text}"
+            ) from exception
+
+        logger.debug(f"Response Body: {response}")
+
+        self.id += 1
+
+        if "error" in response:  # some exception
+            error = response["error"]
+
+            # some errors don't contain 'data': workaround for ZBX-9340
+            if "data" not in error:
+                error["data"] = "No data"
+
+            raise ZabbixAPIException(
+                f"Error {error['code']}: {error['message']}, {error['data']}",
+                error["code"],
+                error=error,
+            )
+
+        return response
+
+    def _object(self, attr: str) -> "ZabbixAPIObject":
+        """Dynamically create an object class (ie: host)"""
+        return ZabbixAPIObject(attr, self)
+
+    def __getattr__(self, attr: str) -> "ZabbixAPIObject":
+        return self._object(attr)
+
+    def __getitem__(self, attr: str) -> "ZabbixAPIObject":
+        return self._object(attr)
+
+
+# pylint: disable=too-few-public-methods
+class ZabbixAPIMethod:
+    def __init__(self, method: str, parent: ZabbixAPI):
+        self._method = method
+        self._parent = parent
+
+    def __call__(self, *args, **kwargs):
+        if args and kwargs:
+            raise TypeError("Found both args and kwargs")
+
+        return self._parent.do_request(self._method, args or kwargs)["result"]
+
+
+# pylint: disable=too-few-public-methods
+class ZabbixAPIObject:
+    def __init__(self, name: str, parent: ZabbixAPI):
+        self._name = name
+        self._parent = parent
+
+    def _method(self, attr: str) -> ZabbixAPIMethod:
+        """Dynamically create a method (ie: get)"""
+        return ZabbixAPIMethod(f"{self._name}.{attr}", self._parent)
+
+    def __getattr__(self, attr: str) -> ZabbixAPIMethod:
+        return self._method(attr)
+
+    def __getitem__(self, attr: str) -> ZabbixAPIMethod:
+        return self._method(attr)
+
+
+class ZabbixAPIObjectClass(ZabbixAPIObject):
+    def __init__(self, *args, **kwargs):
+        warn(
+            "ZabbixAPIObjectClass has been renamed to ZabbixAPIObject",
+            DeprecationWarning,
+            2,
+        )
+        super().__init__(*args, **kwargs)
diff --git a/pyzabbix/py.typed b/pyzabbix/py.typed
new file mode 100644
index 0000000..1242d43
--- /dev/null
+++ b/pyzabbix/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561.
diff --git a/scripts/release.sh b/scripts/release.sh
new file mode 100755
index 0000000..698494d
--- /dev/null
+++ b/scripts/release.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+
+set -u
+
+error() {
+  echo >&2 "error: $*"
+  exit 1
+}
+
+# This scripts will:
+# - Expect the setup.py file to have new version
+# - Stash the setup.py file
+# - Check for clean state (no uncommitted changes), exit if failed
+# - Clean the project
+# - Install the project, lint and runt tests, exit if failed
+# - Unstash the pyproject version bump and commit a new Release
+# - Tag the new release
+# - Show instruction to push tags and changes to github
+
+command -v make > /dev/null || error "make command not found!"
+command -v git > /dev/null || error "git command not found!"
+command -v python3 > /dev/null || error "python3 command not found!"
+
+setup="setup.py"
+default_branch="master"
+
+[[ "$(git rev-parse --show-toplevel)" == "$(pwd)" ]] || error "please go to the project root directory!"
+[[ "$(git rev-parse --abbrev-ref HEAD)" == "$default_branch" ]] || error "please change to the $default_branch git branch!"
+
+pkg_version=$(python3 setup.py --version || error "could not determine package version in $setup!")
+git_version=$(git describe --abbrev=0 --tags || error "could not determine git version!")
+
+# No version change
+if [[ "$pkg_version" == "$git_version" ]]; then
+    echo "Latest git tag '$pkg_version' and package version '$git_version' match, edit your $setup to change the version before running this script!"
+    exit
+fi
+
+git stash push --quiet -- "$setup"
+trap 'e=$?; git stash pop --quiet; exit $e' EXIT
+
+[[ -z "$(git status --porcelain)" ]] || error "please commit or clean the changes before running this script!"
+
+git clean -xdf
+
+make lint test || error "tests failed, please correct the errors"
+
+new_tag="$pkg_version"
+release="chore: release $new_tag"
+
+git stash pop --quiet
+git add "$setup" || error "could not stage $setup!"
+git commit -m "$release" --no-verify || error "could not commit the version bump!"
+git tag "$new_tag" -a -m "$release" || error "could not tag the version bump!"
+
+echo "Run 'git push --follow-tags' in order to publish the release on Github!"
diff --git a/setup.py b/setup.py
index 8536711..a0bde9a 100644
--- a/setup.py
+++ b/setup.py
@@ -1,14 +1,11 @@
 from setuptools import setup
 
-with open("README.markdown", "r") as fh:
+with open("README.md", encoding="utf-8") as fh:
     long_description = fh.read()
 
 setup(
     name="pyzabbix",
-    version="0.8.2",
-    install_requires=[
-        "requests>=1.0",
-    ],
+    version="1.3.0",
     description="Zabbix API Python interface",
     long_description=long_description,
     long_description_content_type="text/markdown",
@@ -19,11 +16,12 @@ setup(
     url="http://github.com/lukecyca/pyzabbix",
     classifiers=[
         "Programming Language :: Python",
-        "Programming Language :: Python :: 2.7",
-	"Programming Language :: Python :: 3.4",
-        "Programming Language :: Python :: 3,5",
-	"Programming Language :: Python :: 3.6",
-	"Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
         "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
         "Operating System :: OS Independent",
         "Development Status :: 4 - Beta",
@@ -33,7 +31,23 @@ setup(
         "Topic :: System :: Systems Administration",
     ],
     packages=["pyzabbix"],
-    tests_require=[
-        "httpretty<0.8.7",
+    package_data={"": ["py.typed"]},
+    python_requires=">=3.6",
+    install_requires=[
+        "requests>=1.0",
+        "packaging",
     ],
+    extras_require={
+        "dev": [
+            "black",
+            "isort",
+            "mypy",
+            "pylint",
+            "pytest-cov",
+            "pytest-xdist",
+            "pytest",
+            "requests-mock",
+            "types-requests",
+        ],
+    },
 )
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/api_test.py b/tests/api_test.py
new file mode 100644
index 0000000..1effe85
--- /dev/null
+++ b/tests/api_test.py
@@ -0,0 +1,285 @@
+import pytest
+from packaging.version import Version
+
+from pyzabbix import ZabbixAPI, ZabbixAPIException
+
+
+@pytest.mark.parametrize(
+    "server, expected",
+    [
+        ("http://example.com", "http://example.com/api_jsonrpc.php"),
+        ("http://example.com/", "http://example.com/api_jsonrpc.php"),
+        ("http://example.com/base", "http://example.com/base/api_jsonrpc.php"),
+        ("http://example.com/base/", "http://example.com/base/api_jsonrpc.php"),
+    ],
+)
+def test_server_url_correction(server, expected):
+    assert ZabbixAPI(server).url == expected
+
+
+def _zabbix_requests_mock_factory(requests_mock, *args, **kwargs):
+    requests_mock.post(
+        "http://example.com/api_jsonrpc.php",
+        request_headers={
+            "Content-Type": "application/json-rpc",
+            "User-Agent": "python/pyzabbix",
+            "Cache-Control": "no-cache",
+        },
+        *args,
+        **kwargs,
+    )
+
+
+def test_login(requests_mock):
+    _zabbix_requests_mock_factory(
+        requests_mock,
+        json={
+            "jsonrpc": "2.0",
+            "result": "0424bd59b807674191e7d77572075f33",
+            "id": 0,
+        },
+    )
+
+    zapi = ZabbixAPI("http://example.com", detect_version=False)
+    zapi.login("mylogin", "mypass")
+
+    # Check request
+    assert requests_mock.last_request.json() == {
+        "jsonrpc": "2.0",
+        "method": "user.login",
+        "params": {"user": "mylogin", "password": "mypass"},
+        "id": 0,
+    }
+
+    # Check login
+    assert zapi.auth == "0424bd59b807674191e7d77572075f33"
+
+
+def test_login_with_context(requests_mock):
+    _zabbix_requests_mock_factory(
+        requests_mock,
+        json={
+            "jsonrpc": "2.0",
+            "result": "0424bd59b807674191e7d77572075f33",
+            "id": 0,
+        },
+    )
+
+    with ZabbixAPI("http://example.com", detect_version=False) as zapi:
+        zapi.login("mylogin", "mypass")
+        assert zapi.auth == "0424bd59b807674191e7d77572075f33"
+
+
+@pytest.mark.parametrize(
+    "version",
+    [
+        ("4.0.0"),
+        ("5.4.0"),
+        ("6.2.0"),
+        ("6.2.0beta1"),
+        ("6.2.2alpha1"),
+    ],
+)
+def test_login_with_version_detect(requests_mock, version):
+    _zabbix_requests_mock_factory(
+        requests_mock,
+        [
+            {
+                "json": {
+                    "jsonrpc": "2.0",
+                    "result": version,
+                    "id": 0,
+                }
+            },
+            {
+                "json": {
+                    "jsonrpc": "2.0",
+                    "result": "0424bd59b807674191e7d77572075f33",
+                    "id": 0,
+                }
+            },
+        ],
+    )
+
+    with ZabbixAPI("http://example.com") as zapi:
+        zapi.login("mylogin", "mypass")
+        assert zapi.auth == "0424bd59b807674191e7d77572075f33"
+
+
+def test_attr_syntax_kwargs(requests_mock):
+    _zabbix_requests_mock_factory(
+        requests_mock,
+        json={
+            "jsonrpc": "2.0",
+            "result": [{"hostid": 1234}],
+            "id": 0,
+        },
+    )
+
+    zapi = ZabbixAPI("http://example.com", detect_version=False)
+    zapi.auth = "some_auth_key"
+    result = zapi.host.get(hostids=5)
+
+    # Check request
+    assert requests_mock.last_request.json() == {
+        "jsonrpc": "2.0",
+        "method": "host.get",
+        "params": {"hostids": 5},
+        "auth": "some_auth_key",
+        "id": 0,
+    }
+
+    # Check response
+    assert result == [{"hostid": 1234}]
+
+
+def test_attr_syntax_args(requests_mock):
+    _zabbix_requests_mock_factory(
+        requests_mock,
+        json={
+            "jsonrpc": "2.0",
+            "result": {"itemids": ["22982", "22986"]},
+            "id": 0,
+        },
+    )
+
+    zapi = ZabbixAPI("http://example.com", detect_version=False)
+    zapi.auth = "some_auth_key"
+    result = zapi.host.delete("22982", "22986")
+
+    # Check request
+    assert requests_mock.last_request.json() == {
+        "jsonrpc": "2.0",
+        "method": "host.delete",
+        "params": ["22982", "22986"],
+        "auth": "some_auth_key",
+        "id": 0,
+    }
+
+    # Check response
+    assert result == {"itemids": ["22982", "22986"]}
+
+
+def test_attr_syntax_args_and_kwargs_raises():
+    with pytest.raises(
+        TypeError,
+        match="Found both args and kwargs",
+    ):
+        zapi = ZabbixAPI("http://example.com")
+        zapi.host.delete("22982", hostids=5)
+
+
+@pytest.mark.parametrize(
+    "version",
+    [
+        ("4.0.0"),
+        ("4.0.0rc1"),
+        ("6.2.0beta1"),
+        ("6.2.2alpha1"),
+    ],
+)
+def test_detecting_version(requests_mock, version):
+    _zabbix_requests_mock_factory(
+        requests_mock,
+        json={
+            "jsonrpc": "2.0",
+            "result": version,
+            "id": 0,
+        },
+    )
+
+    zapi = ZabbixAPI("http://example.com")
+    zapi.login("mylogin", "mypass")
+
+    assert zapi.api_version() == version
+
+
+@pytest.mark.parametrize(
+    "data",
+    [
+        (None),
+        ('No groups for host "Linux server".'),
+    ],
+)
+def test_error_response(requests_mock, data):
+    _zabbix_requests_mock_factory(
+        requests_mock,
+        json={
+            "jsonrpc": "2.0",
+            "error": {
+                "code": -32602,
+                "message": "Invalid params.",
+                **({} if data is None else {"data": data}),
+            },
+            "id": 0,
+        },
+    )
+
+    with pytest.raises(
+        ZabbixAPIException,
+        match="Error -32602: Invalid params., No data."
+        if data is None
+        else f"Error -32602: Invalid params., {data}",
+    ):
+        zapi = ZabbixAPI("http://example.com")
+        zapi.host.get()
+
+
+def test_empty_response(requests_mock):
+    _zabbix_requests_mock_factory(
+        requests_mock,
+        body="",
+    )
+
+    with pytest.raises(ZabbixAPIException, match="Received empty response"):
+        zapi = ZabbixAPI("http://example.com")
+        zapi.login("mylogin", "mypass")
+
+
+@pytest.mark.parametrize(
+    "version",
+    [
+        ("4.0.0"),
+        ("5.4.0"),
+        ("6.2.0"),
+    ],
+)
+def test_do_request(requests_mock, version):
+    _zabbix_requests_mock_factory(
+        requests_mock,
+        json={
+            "jsonrpc": "2.0",
+            "result": [{"hostid": 1234}],
+            "id": 0,
+        },
+    )
+
+    zapi = ZabbixAPI("http://example.com", detect_version=False)
+    zapi.version = Version(version)
+    zapi.auth = "some_auth_key"
+    result = zapi["host"]["get"]()
+
+    # Check response
+    assert result == [{"hostid": 1234}]
+
+    # Check request
+    found = requests_mock.last_request
+    expect_json = {
+        "jsonrpc": "2.0",
+        "method": "host.get",
+        "params": {},
+        "id": 0,
+    }
+    expect_headers = {
+        "Cache-Control": "no-cache",
+        "Content-Type": "application/json-rpc",
+        "User-Agent": "python/pyzabbix",
+    }
+
+    if zapi.version < Version("6.4.0"):
+        expect_json["auth"] = "some_auth_key"
+    else:
+        expect_headers["Authorization"] = "Bearer some_auth_key"
+
+    assert found.json() == expect_json
+    assert found.headers.items() >= expect_headers.items()
diff --git a/tests/test_api.py b/tests/test_api.py
deleted file mode 100644
index 9131d5e..0000000
--- a/tests/test_api.py
+++ /dev/null
@@ -1,129 +0,0 @@
-import unittest
-import httpretty
-import json
-from pyzabbix import ZabbixAPI
-
-
-class TestPyZabbix(unittest.TestCase):
-
-    @httpretty.activate
-    def test_login(self):
-        httpretty.register_uri(
-            httpretty.POST,
-            "http://example.com/api_jsonrpc.php",
-            body=json.dumps({
-                "jsonrpc": "2.0",
-                "result": "0424bd59b807674191e7d77572075f33",
-                "id": 0
-            }),
-        )
-
-        zapi = ZabbixAPI('http://example.com')
-        zapi.login('mylogin', 'mypass')
-
-        # Check request
-        self.assertEqual(
-            json.loads(httpretty.last_request().body.decode('utf-8')),
-            {
-                'jsonrpc': '2.0',
-                'method': 'user.login',
-                'params': {'user': 'mylogin', 'password': 'mypass'},
-                'id': 0,
-            }
-        )
-        self.assertEqual(
-            httpretty.last_request().headers['content-type'],
-            'application/json-rpc'
-        )
-        self.assertEqual(
-            httpretty.last_request().headers['user-agent'],
-            'python/pyzabbix'
-        )
-
-        # Check response
-        self.assertEqual(zapi.auth, "0424bd59b807674191e7d77572075f33")
-
-    @httpretty.activate
-    def test_host_get(self):
-        httpretty.register_uri(
-            httpretty.POST,
-            "http://example.com/api_jsonrpc.php",
-            body=json.dumps({
-                "jsonrpc": "2.0",
-                "result": [{"hostid": 1234}],
-                "id": 0
-            }),
-        )
-
-        zapi = ZabbixAPI('http://example.com')
-        zapi.auth = "123"
-        result = zapi.host.get()
-
-        # Check request
-        self.assertEqual(
-            json.loads(httpretty.last_request().body.decode('utf-8')),
-            {
-                'jsonrpc': '2.0',
-                'method': 'host.get',
-                'params': {},
-                'auth': '123',
-                'id': 0,
-            }
-        )
-
-        # Check response
-        self.assertEqual(result, [{"hostid": 1234}])
-
-    @httpretty.activate
-    def test_host_delete(self):
-        httpretty.register_uri(
-            httpretty.POST,
-            "http://example.com/api_jsonrpc.php",
-            body=json.dumps({
-                "jsonrpc": "2.0",
-                "result": {
-                    "itemids": [
-                        "22982",
-                        "22986"
-                    ]
-                },
-                "id": 0
-            }),
-        )
-
-        zapi = ZabbixAPI('http://example.com')
-        zapi.auth = "123"
-        result = zapi.host.delete("22982", "22986")
-
-        # Check request
-        self.assertEqual(
-            json.loads(httpretty.last_request().body.decode('utf-8')),
-            {
-                'jsonrpc': '2.0',
-                'method': 'host.delete',
-                'params': ["22982", "22986"],
-                'auth': '123',
-                'id': 0,
-            }
-        )
-
-        # Check response
-        self.assertEqual(set(result["itemids"]), set(["22982", "22986"]))
-
-
-    @httpretty.activate
-    def test_login_with_context(self):
-        httpretty.register_uri(
-            httpretty.POST,
-            "http://example.com/api_jsonrpc.php",
-            body=json.dumps({
-                "jsonrpc": "2.0",
-                "result": "0424bd59b807674191e7d77572075f33",
-                "id": 0
-            }),
-        )
-
-        with ZabbixAPI('http://example.com') as zapi:
-            zapi.login('mylogin', 'mypass')
-            self.assertEqual(zapi.auth, "0424bd59b807674191e7d77572075f33")
-

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix-1.3.0.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix-1.3.0.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix-1.3.0.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix-1.3.0.egg-info/top_level.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix/api.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix/py.typed
-rw-r--r--  root/root   /usr/share/doc/python3-pyzabbix/changelog.gz

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix-0.8.2.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix-0.8.2.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix-0.8.2.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/pyzabbix-0.8.2.egg-info/top_level.txt

Control files: lines which differ (wdiff format)

  • Depends: python3-requests, python3-packaging, python3:any

More details

Full run details