├── tests ├── __init__.py ├── test_common.py ├── test_firmware_downloader.py ├── test_firmware_uploader.py ├── test_utils.py ├── test_snmp_info.py ├── test_models.py └── test_autodiscovery.py ├── .mypy.ini ├── .gitignore ├── .flake8 ├── .pyre_configuration ├── .editorconfig ├── Containerfile ├── tox.ini ├── .vscode └── launch.json ├── brother_printer_fwupd ├── __init__.py ├── models.py ├── common.py ├── firmware_uploader.py ├── snmp_info.py ├── __main__.py ├── autodiscovery.py ├── utils.py └── firmware_downloader.py ├── simulator ├── pyproject.toml ├── README.md ├── data │ └── public.snmprec ├── run.py └── uv.lock ├── check.sh ├── .pre-commit-config.yaml ├── pyproject.toml ├── README.md ├── pylintrc └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.djf 2 | .venv 3 | *.egg-info/ 4 | dist/ 5 | __pycache__ 6 | .pyre/ 7 | .tox/ 8 | -------------------------------------------------------------------------------- /tests/test_common.py: -------------------------------------------------------------------------------- 1 | """Test of the common module.""" 2 | 3 | # TODO write some tests. 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=100 3 | ignore = D203, D212, D105, W503 4 | # vi: ft=cfg 5 | -------------------------------------------------------------------------------- /tests/test_firmware_downloader.py: -------------------------------------------------------------------------------- 1 | """Unit tests for firmware_downloader.""" 2 | 3 | # TODO write some tests. 4 | -------------------------------------------------------------------------------- /tests/test_firmware_uploader.py: -------------------------------------------------------------------------------- 1 | """Unit tests for firmware uploader.""" 2 | 3 | # TODO write some tests. 4 | -------------------------------------------------------------------------------- /.pyre_configuration: -------------------------------------------------------------------------------- 1 | { 2 | "site_package_search_strategy": "pep561", 3 | "source_directories": [ 4 | "brother_printer_fwupd" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_size = 2 10 | 11 | [*.py] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | 3 | RUN pip install --upgrade pipx \ 4 | && pipx install brother-printer-fwupd[autodiscover] 5 | 6 | CMD brother-printer-fwupd 7 | 8 | ENV PATH="/root/.local/bin/:${PATH}" 9 | 10 | MAINTAINER sedrubal 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | env_list = py{39, 310, 311,312, 313, 314} 3 | minversion = 4.15.0 4 | 5 | [testenv] 6 | description = run the tests with pytest 7 | package = wheel 8 | wheel_build_env = .pkg 9 | deps = 10 | pytest>=6 11 | commands = 12 | pytest {tty:--color=yes} {posargs} 13 | extras = autodiscover 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: Module", 6 | "type": "python", 7 | "request": "launch", 8 | "module": "brother_printer_fwupd", 9 | "justMyCode": true, 10 | "args": [], 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /brother_printer_fwupd/__init__.py: -------------------------------------------------------------------------------- 1 | """Tool to update the firmware of some Brother printers (e. g. MFC).""" 2 | 3 | import importlib.metadata as importlib_metadata 4 | 5 | try: 6 | __version__ = importlib_metadata.version(__name__) 7 | except importlib_metadata.PackageNotFoundError: 8 | __version__ = "unknown" 9 | 10 | 11 | ISSUE_URL = "https://github.com/sedrubal/brother_printer_fwupd/issues/new" 12 | -------------------------------------------------------------------------------- /simulator/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "brother-printer-fwupd-snmp-simulator" 3 | version = "0.1.0" 4 | description = "SNMP simulator for Brother Printers" 5 | authors = [ 6 | {name = "sedrubal", email = "dev@sedrubal.de"}, 7 | ] 8 | license = {text = "GPL-3.0-or-later"} 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | dependencies = [ 12 | "snmpsim>=1.1.7", 13 | "zeroconf>=0.136.2", 14 | ] 15 | classifiers = ["Private :: Do not upload"] 16 | 17 | [tool.ruff] 18 | line-length = 100 19 | -------------------------------------------------------------------------------- /simulator/README.md: -------------------------------------------------------------------------------- 1 | # Brother Printer MDNS & SNMP Simulator 2 | 3 | MDNS and SNMP simulator for Brother Printers using 4 | [Zeroconf](https://github.com/python-zeroconf/python-zeroconf) and 5 | [snmpsim](https://docs.lextudio.com/snmpsim/). 6 | 7 | ## Run the simulator 8 | 9 | ```bash 10 | uv run ./run.py 11 | ``` 12 | 13 | ## Test the simulator 14 | 15 | ```bash 16 | # Test MDNS 17 | avahi-resolve --name MFC-9332CDW._pdl-datastream._tcp.local 18 | avahi-browse --all --resolve 19 | # Test SNMP 20 | snmpwalk -v 2c -c public 127.0.0.1:1161 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2 21 | ``` 22 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | PKG="brother_printer_fwupd" 6 | echo "--> Running isort" 7 | uv run isort "${PKG}" 8 | # echo "--> Running black" 9 | # uv run black "${PKG}" 10 | echo "--> Running pylint" 11 | uv run pylint --exit-zero --jobs 0 "${PKG}" 12 | echo "--> Running pyre" 13 | set +e # pyre has no flag to exit with 0 14 | uv run pyre check 15 | set -e 16 | echo "--> Running ruff" 17 | uv run ruff check 18 | uv run ruff format 19 | echo "--> Running mypy" 20 | set +e # mypy has no flag to exit with 0 21 | uv run mypy "${PKG}" 22 | set -e 23 | # echo "--> Running tox" 24 | # uv run tox 25 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """Test the utils module.""" 2 | 3 | import logging 4 | import unittest 5 | 6 | from brother_printer_fwupd.utils import sluggify 7 | 8 | 9 | class TestUtils(unittest.TestCase): 10 | """Test the utils module.""" 11 | 12 | def test_sluggify(self): 13 | """Test the sluggify function.""" 14 | self.assertEqual(sluggify("Hello World!"), "hello_world") 15 | self.assertEqual(sluggify("../foo/bar.exe"), "foobarexe") 16 | 17 | def test_logger(self): 18 | """Test modifications to the logging module.""" 19 | self.assertEqual(logging.SUCCESS, 25) # pylint: disable=no-member 20 | self.assertTrue(hasattr(logging.Logger, "success")) 21 | -------------------------------------------------------------------------------- /tests/test_snmp_info.py: -------------------------------------------------------------------------------- 1 | """Unit tests for SNMP info.""" 2 | 3 | import ipaddress 4 | import unittest 5 | 6 | from brother_printer_fwupd.models import FWInfo 7 | from brother_printer_fwupd.snmp_info import get_snmp_info_sync 8 | 9 | 10 | class TestSNMPInfo(unittest.TestCase): 11 | """Test the snmp_info module.""" 12 | 13 | def test_get_snmp_info_sync(self): 14 | """ 15 | Test the get_snmp_info_sync function. 16 | 17 | This requires that the simulator is running. 18 | """ 19 | info = get_snmp_info_sync( 20 | target=ipaddress.IPv4Address("127.0.0.1"), 21 | port=1161, 22 | ) 23 | self.assertEqual(info.model, "MFC-9332CDW") 24 | self.assertEqual(info.serial, "E01234A5J678901") 25 | self.assertEqual(info.spec, "0403") 26 | self.assertEqual( 27 | info.fw_versions, 28 | [ 29 | FWInfo(firmid="MAIN", firmver="R2311081154:E7E5"), 30 | FWInfo(firmid="SUB1", firmver="1.05"), 31 | FWInfo(firmid="SUB2", firmver="R2311081800"), 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | --- 4 | 5 | repos: 6 | # - repo: https://github.com/psf/black 7 | # rev: 24.10.0 8 | # hooks: 9 | # - id: black 10 | # language_version: python3 11 | 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: v0.8.6 14 | hooks: 15 | - id: ruff 16 | # Run the linter. 17 | args: [--fix, --exit-non-zero-on-fix] 18 | - id: ruff-format 19 | # Run the formatter. 20 | 21 | - repo: https://github.com/pre-commit/mirrors-mypy 22 | rev: v1.14.1 23 | hooks: 24 | - id: mypy 25 | additional_dependencies: 26 | - types-requests>=2.31.0 27 | - types-termcolor>=1.1.6 28 | 29 | - repo: https://github.com/PyCQA/isort 30 | rev: 5.13.2 31 | hooks: 32 | - id: isort 33 | 34 | - repo: https://github.com/pre-commit/pre-commit-hooks 35 | rev: v5.0.0 36 | hooks: 37 | - id: trailing-whitespace 38 | exclude: '^.*\.(md|snmprec)$' 39 | # - id: debug-statements 40 | - id: end-of-file-fixer 41 | - id: check-yaml 42 | - id: check-added-large-files 43 | # - id: flake8 44 | - id: mixed-line-ending 45 | args: ["--fix=lf"] 46 | exclude: '^.*\.bat$' 47 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | """Test the data structures.""" 2 | 3 | import argparse 4 | import unittest 5 | 6 | from brother_printer_fwupd.models import FWInfo, SNMPPrinterInfo 7 | 8 | 9 | class TestModels(unittest.TestCase): 10 | """Test the models file.""" 11 | 12 | def test_fwinfo(self): 13 | """Test the FWInfo model.""" 14 | firmid = "MAIN" 15 | firmver = "10.1.1" 16 | value = f"{firmid}@{firmver}" 17 | fw = FWInfo.from_str(value) 18 | self.assertEqual(str(fw), value) 19 | self.assertEqual(fw.firmid, firmid) 20 | self.assertEqual(fw.firmver, firmver) 21 | 22 | def test_snmp_printer_info(self): 23 | """Test the SNMPPrinterInfo model.""" 24 | model = "DUMMY" 25 | serial = "E01234A5J678901" 26 | spec = "0403" 27 | fw_version = FWInfo.from_str("SUB2@R2311081154:E7E5") 28 | 29 | args = argparse.Namespace() 30 | args.model = model 31 | args.serial = serial 32 | args.spec = spec 33 | args.fw_versions = [fw_version] 34 | 35 | printer_info = SNMPPrinterInfo.from_args(args) 36 | 37 | self.assertEqual(printer_info.model, model) 38 | self.assertEqual(printer_info.serial, serial) 39 | self.assertEqual(printer_info.spec, spec) 40 | self.assertEqual(printer_info.fw_versions, [fw_version]) 41 | -------------------------------------------------------------------------------- /tests/test_autodiscovery.py: -------------------------------------------------------------------------------- 1 | """Unittests for autodiscover module.""" 2 | 3 | import ipaddress 4 | import socket 5 | import unittest 6 | 7 | import zeroconf 8 | 9 | from brother_printer_fwupd.autodiscovery import ( 10 | ZEROCONF_SERVICE_DOMAIN, 11 | PrinterDiscoverer, 12 | ) 13 | 14 | # pylint: disable=protected-access 15 | 16 | 17 | class TestAutodiscovery(unittest.TestCase): 18 | """Test the PrinterDiscoverer class.""" 19 | 20 | def test_printer_discoverer(self): 21 | """Test the PrinterDiscoverer class.""" 22 | discoverer = PrinterDiscoverer() 23 | 24 | address = ipaddress.IPv4Address("127.0.0.1") 25 | pdl_ds_port = 8080 26 | 27 | class DummyZeroconf: 28 | def get_service_info(self, type_: str, name: str): 29 | return zeroconf.ServiceInfo( 30 | type_, 31 | name, 32 | addresses=[socket.inet_aton(str(address))], 33 | port=pdl_ds_port, 34 | properties={ 35 | b"product": b"DUMMY", 36 | b"note": b"Printer", 37 | }, 38 | ) 39 | 40 | zc = DummyZeroconf() 41 | name = f"DUMMY.{ZEROCONF_SERVICE_DOMAIN}" 42 | discoverer.add_service(zc, ZEROCONF_SERVICE_DOMAIN, name) 43 | 44 | self.assertEqual(discoverer._printers[0].name, name) 45 | self.assertEqual(discoverer._printers[0].ip_addr, address) 46 | self.assertEqual(discoverer._printers[0].port, pdl_ds_port) 47 | self.assertEqual(discoverer._printers[0].product, "DUMMY") 48 | self.assertEqual(discoverer._printers[0].note, "Printer") 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = [ 3 | {name = "sedrubal", email = "dev@sedrubal.de"}, 4 | ] 5 | license = {text = "GPL-3.0-or-later"} 6 | requires-python = "<4.0,>=3.9" 7 | dependencies = [ 8 | "requests>=2.32.3", 9 | "beautifulsoup4>=4.12.3", 10 | "lxml>=5.3.0", 11 | "termcolor>=2.5.0", 12 | "pysnmp>=7.1.15", 13 | ] 14 | name = "brother_printer_fwupd" 15 | version = "0.8.0" 16 | description = "Script to update the firmware of some Brother printers (e. g. MFC)." 17 | readme = "README.md" 18 | 19 | [project.urls] 20 | repository = "https://github.com/sedrubal/brother_printer_fwupd.git" 21 | 22 | [build-system] 23 | requires = ["pdm-backend"] 24 | build-backend = "pdm.backend" 25 | 26 | [dependency-groups] 27 | dev = [ 28 | "black<25.0.0,>=24.10.0", 29 | "isort<6.0.0,>=5.13.2", 30 | "mypy>=1.14.0,<2.0.0", 31 | "pre-commit<4.0.0,>=3.8.0", 32 | "pylint>=3.3.1,<4.0.0", 33 | "pyre-check<1.0.0,>=0.9.22", 34 | "ruff>=0.8.6,<1.0.0", 35 | "tox-uv>=1.16.1", 36 | "tox<5.0.0,>=4.15.0", 37 | "types-requests<3.0.0,>=2.31.0", 38 | "types-termcolor<2.0.0,>=1.1.6", 39 | ] 40 | 41 | [project.optional-dependencies] 42 | autodiscover = [ 43 | "zeroconf<1.0.0,>=0.136.0", 44 | ] 45 | 46 | [project.scripts] 47 | brother_printer_fwupd = "brother_printer_fwupd.__main__:main" 48 | brother-printer-fwupd = "brother_printer_fwupd.__main__:main" 49 | brother-printer-fwupd-snmp-info = "brother_printer_fwupd.snmp_info:main" 50 | brother-printer-fwupd-autodiscover = "brother_printer_fwupd.autodiscovery:main" 51 | brother-printer-fwupd-download = "brother_printer_fwupd.firmware_downloader:main" 52 | brother-printer-fwupd-upload = "brother_printer_fwupd.firmware_uploader:main" 53 | 54 | [tool.isort] 55 | profile = "black" 56 | 57 | [tool.ruff] 58 | line-length = 100 59 | -------------------------------------------------------------------------------- /brother_printer_fwupd/models.py: -------------------------------------------------------------------------------- 1 | """Types and model classes / definitions.""" 2 | 3 | import argparse 4 | import ipaddress 5 | import typing 6 | from dataclasses import dataclass, field 7 | 8 | import termcolor 9 | 10 | IPAddress = typing.Union[ipaddress.IPv4Address, ipaddress.IPv6Address] 11 | 12 | 13 | @dataclass 14 | class FWInfo: 15 | """Firmware fragment info.""" 16 | 17 | firmid: str 18 | firmver: str 19 | 20 | def __str__(self): 21 | return f"{self.firmid}@{self.firmver}" 22 | 23 | @classmethod 24 | def from_str(cls, value: str): 25 | """Parse FW info from string from command line argument.""" 26 | try: 27 | firmid, firmver = value.split("@", 1) 28 | except ValueError as err: 29 | raise argparse.ArgumentTypeError( 30 | termcolor.colored( 31 | f"Invalid firmware ID {value}. Format: firmid@firmver", 32 | "red", 33 | ) 34 | ) from err 35 | return cls(firmid, firmver) 36 | 37 | 38 | @dataclass 39 | class SNMPPrinterInfo: 40 | """Information about a printer.""" 41 | 42 | model: typing.Optional[str] = field(default=None) 43 | serial: typing.Optional[str] = field(default=None) 44 | spec: typing.Optional[str] = field(default=None) 45 | fw_versions: list[FWInfo] = field(default_factory=list) 46 | 47 | @classmethod 48 | def from_args(cls, args: argparse.Namespace) -> "SNMPPrinterInfo": 49 | """Create a printer info instance from command line arguments.""" 50 | return cls( 51 | model=args.model, 52 | serial=args.serial, 53 | spec=args.spec, 54 | fw_versions=args.fw_versions, 55 | ) 56 | 57 | 58 | @dataclass 59 | class MDNSPrinterInfo: 60 | """Information about a printer received via MDNS.""" 61 | 62 | ip_addr: IPAddress 63 | name: str 64 | port: typing.Optional[int] 65 | product: typing.Optional[str] 66 | note: typing.Optional[str] 67 | uuid: typing.Optional[str] 68 | -------------------------------------------------------------------------------- /simulator/data/public.snmprec: -------------------------------------------------------------------------------- 1 | # Dump of a MFC-9332CDW 2 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.1|1|1 3 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.2|1|2 4 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.3|1|3 5 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.4|1|4 6 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.5|1|5 7 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.6|1|6 8 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.7|1|7 9 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.8|1|8 10 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.9|1|9 11 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.10|1|10 12 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.11|1|11 13 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.12|1|12 14 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.13|1|13 15 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.14|1|14 16 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.15|1|15 17 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.1.16|1|16 18 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.1|4|MODEL="MFC-9332CDW" 19 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.2|4|CTYPE = "MFC" 20 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.3|4|SERIAL="E01234A5J678901" 21 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.4|4|SPEC="0403" 22 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.5|4|FIRMID="MAIN" 23 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.6|4|FIRMVER="R2311081154:E7E5" 24 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.7|4|FIRMID="SUB1" 25 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.8|4|FIRMVER="1.05" 26 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.9|4|FIRMID="SUB2" 27 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.10|4|FIRMVER="R2311081800" 28 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.11|4| 29 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.12|4| 30 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.13|4| 31 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.14|4| 32 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.15|4| 33 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.16|4| 34 | 1.3.6.1.4.1.2435.2.4.3.99.3.1.16.0|1|16 35 | 36 | # Test Dump 37 | # 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.1|4|MODEL = "MFC-7860DW" 38 | # 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.2|4|SERIAL = "1234" 39 | # 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.3|4|SPEC = "0706" 40 | # 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.4|4|FIRMID = "MAIN" 41 | # 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2.5|4|FIRMVER = "W2109141139:E619" 42 | -------------------------------------------------------------------------------- /brother_printer_fwupd/common.py: -------------------------------------------------------------------------------- 1 | """Common helper functions.""" 2 | 3 | import argparse 4 | 5 | from .models import FWInfo 6 | 7 | 8 | def common_args(parser: argparse.ArgumentParser, ip_required: bool): 9 | """Add common args to a argparse parser.""" 10 | from . import __version__ # pylint: disable=import-outside-toplevel 11 | 12 | if ip_required: 13 | blurb = "required, because zeroconf is not available" 14 | else: 15 | blurb = "default: autodiscover via mdns" 16 | 17 | parser.add_argument( 18 | "--ip", 19 | "--printer", 20 | required=ip_required, 21 | dest="ip", 22 | metavar="host", 23 | default=None, 24 | help=f"IP Address or hostname of the printer ({blurb})).", 25 | ) 26 | parser.add_argument( 27 | "--debug", 28 | dest="debug", 29 | action="store_true", 30 | help="Print debug messages", 31 | ) 32 | parser.add_argument( 33 | "--version", 34 | action="version", 35 | version=f"%(prog)s {__version__}", 36 | ) 37 | 38 | 39 | def printer_info_args(parser: argparse.ArgumentParser, required: bool = False) -> None: 40 | """ 41 | Add arguments with printer info to an argparse parser. 42 | 43 | These arguments are required, if SNMP was skipped. 44 | """ 45 | parser.add_argument( 46 | "--model", 47 | dest="model", 48 | required=required, 49 | type=str, 50 | help="Skip SNMP scanning by directly specifying the printer model.", 51 | ) 52 | parser.add_argument( 53 | "--serial", 54 | dest="serial", 55 | required=required, 56 | type=str, 57 | help="Skip SNMP scanning by directly specifying the printer serial.", 58 | ) 59 | parser.add_argument( 60 | "--spec", 61 | dest="spec", 62 | required=required, 63 | type=str, 64 | help="Skip SNMP scanning by directly specifying the printer spec.", 65 | ) 66 | parser.add_argument( 67 | "--fw", 68 | "--fw-versions", 69 | dest="fw_versions", 70 | nargs="+" if required else "*", 71 | default=None if required else [], # In Python 3.10+: list[FWInfo]() 72 | required=required, 73 | type=FWInfo.from_str, 74 | help="Skip SNMP scanning by directly specifying the firmware parts to update.", 75 | ) 76 | -------------------------------------------------------------------------------- /brother_printer_fwupd/firmware_uploader.py: -------------------------------------------------------------------------------- 1 | """Upload firmware to the brinter.""" 2 | 3 | import argparse 4 | import logging 5 | import socket 6 | from pathlib import Path 7 | 8 | from .common import common_args 9 | from .models import IPAddress 10 | from .utils import ( 11 | CONSOLE_LOG_HANDLER, 12 | DEFAULT_PDL_DATASTREAM_PORT, 13 | LOGGER, 14 | get_default_port, 15 | ) 16 | 17 | 18 | def upload_fw( 19 | target: IPAddress, 20 | port: int = DEFAULT_PDL_DATASTREAM_PORT, 21 | fw_file_path: Path = Path("firmware.djf"), 22 | ): 23 | """ 24 | Upload the firmware to the printer via PDL Datastream / JetDirect. 25 | 26 | Equals: 27 | ``` 28 | cat LZ5413_P.djf | nc lp.local 9100 29 | ``` 30 | """ 31 | 32 | LOGGER.info( 33 | "Uploading firmware file %s to printer via jetdirect at %s:%i.", 34 | fw_file_path, 35 | target, 36 | port, 37 | ) 38 | 39 | addr_info = socket.getaddrinfo(str(target), port, 0, 0, socket.SOL_TCP)[0] 40 | 41 | with socket.socket(addr_info[0], addr_info[1], 0) as sock: 42 | sock.connect(addr_info[4]) 43 | 44 | with fw_file_path.open("rb") as fw_file: 45 | sock.sendfile(fw_file) 46 | 47 | LOGGER.success("Successfully uploaded the firmware file %s", fw_file_path) 48 | 49 | 50 | def fw_uploader_args(parser: argparse.ArgumentParser, set_pdl_ds_port_default: bool = True) -> None: 51 | """Add command line arguments for the firmware uploader to an argparse parser.""" 52 | if set_pdl_ds_port_default: 53 | default_blurb = "default: %(default)s" 54 | else: 55 | default_blurb = "determined using MDNS autodiscovery" 56 | 57 | parser.add_argument( 58 | "--pdl-ds-port", 59 | dest="pdl_ds_port", 60 | metavar="port", 61 | type=int, 62 | default=get_default_port("pdl-datastream") if set_pdl_ds_port_default else None, 63 | help=f"The TCP port for PDL Datastream / jetdirect at the printer ({default_blurb}).", 64 | ) 65 | 66 | 67 | def parse_args() -> argparse.Namespace: 68 | """Parse command line arguments.""" 69 | parser = argparse.ArgumentParser( 70 | description=__doc__.strip().splitlines()[0], 71 | ) 72 | 73 | common_args(parser, ip_required=True) 74 | fw_uploader_args(parser, set_pdl_ds_port_default=True) 75 | 76 | parser.add_argument( 77 | "fw_file", 78 | type=Path, 79 | help="The firmware file to send to the printer.", 80 | ) 81 | 82 | return parser.parse_args() 83 | 84 | 85 | def main(): 86 | """Run the firmware uploader.""" 87 | args = parse_args() 88 | 89 | CONSOLE_LOG_HANDLER.setLevel(logging.DEBUG if args.debug else logging.INFO) 90 | 91 | upload_fw( 92 | target=args.ip, 93 | port=args.pdl_ds_port, 94 | fw_file_path=args.fw_file, 95 | ) 96 | 97 | 98 | if __name__ == "__main__": 99 | main() 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brother Printer Firmware Update Tool 2 | 3 | Script to update the firmware of some Brother printers (e.g. MFC). 4 | 5 | ## Idea: 6 | 7 | Brother provides drivers etc. for Linux but when you want to update the firmware of your printer, 8 | Brother only provides ugly programs for Windows or Mac OS. 9 | I think, you should do firmware upgrades from time to time, because the supported printers have 10 | advanced network functionality, like SNMP, HTTP Web Management UI, and much more. 11 | Some of them can be connected with Ethernet and some of them with WiFi. 12 | The more features a system has, the greater is the risk of bugs. 13 | 14 | You could try to use the official tools. You could try to run the Windows EXE in Wine for example. 15 | First it seemed to work, but then it crashed - like almost always I tried to run programs in wine. 16 | You could also extract the jar from the Mac OS DMG as described [here](https://avandorp.wordpress.com/2009/07/21/brother-printer-firmware-update-with-linux-brother-druckerfirmware-update-mit-linux/). 17 | But unfortunately this tool didn't find my printer and I had no chance to debug the problems as it 18 | didn't give me any hint, what it is trying to do. 19 | 20 | Then I found blog posts like 21 | [this](https://www.earth.li/~noodles/blog/2015/11/updating-hl3040cn-firmware.html), that described a 22 | way to update the firmware with only standard Linux tools. 23 | I tried it and it worked :tada: 24 | 25 | To make it a bit easier, I wrote this simple script. 26 | 27 | ## See also 28 | 29 | - https://github.com/CauldronDevelopmentLLC/oh-brother 30 | - https://cbompart.wordpress.com/2014/05/26/brother-printer-firmware-part-2/ 31 | 32 | ## What the script does: 33 | 34 | 1. Optional (if no IP is given): Discover the printer using MDNS 35 | 2. Get printer info (like model name, firmware versions, etc.) with SNMP (v2c) 36 | 3. Query the firmware download URL from Brother API server 37 | 4. Download the firmware 38 | 5. Upload the firmware to the printer over port 9100 (PDL-Datastream / jetdirect) 39 | 40 | *Note: Make sure the required protocols are enabled in the printer settings* 41 | 42 | ## Tested with: 43 | 44 | - DCP-9020CDW 45 | - DCP-9022CDW 46 | - HL-5370DW 47 | - MFC-9142CDN 48 | - MFC-9332CDW 49 | - MFC-L3750CDW 50 | 51 | ## Usage 52 | 53 | ### A: Run with docker / podman: 54 | 55 | The image is published on [hub.docker.com](https://hub.docker.com/r/therealsedrubal/brother-printer-fwupd). 56 | 57 | ```bash 58 | podman run --rm -it --network=host therealsedrubal/brother-printer-fwupd 59 | ``` 60 | 61 | ### B: Run package using `pipx`: 62 | 63 | ```shell 64 | pipx run brother-printer-fwupd[autodiscover] 65 | ``` 66 | 67 | ### C: Install package using `pip`: 68 | 69 | ```shell 70 | pip install --user --upgrade brother-printer-fwupd[autodiscover] 71 | ``` 72 | 73 | *If this does not work, try `pip install --user --upgrade brother_printer_fwupd`.* 74 | 75 | ### D: Development installation: 76 | 77 | 1. Clone the repo 78 | 2. Install system dependencies: `libxslt-dev`, `libxml2-dev` 79 | 3. `uv sync --all-extras` 80 | 4. `uv run brother-printer-fwupd --help` 81 | 5. `uv run brother-printer-fwupd-autodiscover --help` 82 | 6. `uv run brother-printer-fwupd-snmp-info --help` 83 | 84 | Use at your own risk!™ 85 | 86 | Contributions welcome. 87 | 88 | ## Unit tests 89 | 90 | Start the simulator in a second terminal: 91 | 92 | ```bash 93 | cd ./simulator/ 94 | uv run ./run.py 95 | ``` 96 | 97 | Make sure, that you have all python interpreter versions installed. Then, run the unit test: 98 | 99 | ```bash 100 | uv run tox 101 | ``` 102 | 103 | ## License 104 | 105 | [© 2025 sedrubal (GPLv3)](./LICENSE) 106 | -------------------------------------------------------------------------------- /simulator/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Run the printer simulator.""" 4 | 5 | import socket 6 | import sys 7 | import threading 8 | import time 9 | import uuid 10 | from hashlib import sha512 11 | 12 | from snmpsim.commands.responder import main as snmpsim_main 13 | from zeroconf import InterfaceChoice, ServiceInfo, Zeroconf 14 | 15 | PRINTER_IP = "127.0.0.1" 16 | PDL_DATASTREAM_PORT = 9100 17 | SNMP_PORT = 1161 18 | 19 | PDL_BUF_SIZE = 1024 20 | 21 | 22 | class PdlDsStreamServer(threading.Thread): 23 | """A single threaded blocking TCP server as a Thread.""" 24 | 25 | def __init__(self): 26 | self._server_socket = socket.create_server( 27 | (PRINTER_IP, PDL_DATASTREAM_PORT), reuse_port=True 28 | ) 29 | self._stopping = False 30 | super().__init__(target=self.target) 31 | 32 | def start(self): 33 | self._server_socket.listen() 34 | self._stopping = False 35 | print( 36 | f"PDL Datastream server is listening at TCP/IPv4 endpoint {PRINTER_IP}:{PDL_DATASTREAM_PORT}" 37 | ) 38 | super().start() 39 | 40 | def target(self): 41 | """Thread function.""" 42 | 43 | while True: 44 | client_socket, client_info = self._server_socket.accept() 45 | 46 | if self._stopping: 47 | print("Shutting down PDL Datastream server...") 48 | client_socket.close() 49 | 50 | break 51 | self.__class__.handle_connection(client_socket, client_info) 52 | 53 | @staticmethod 54 | def handle_connection(client_socket: socket.socket, client_info: tuple[str, int]) -> None: 55 | """Handle a client connection.""" 56 | checksum = sha512() 57 | size = 0 58 | print(f"Accepted connection from {client_info[0]}:{client_info[1]}") 59 | print('"Updating..."') 60 | 61 | while True: 62 | chunk = client_socket.recv(PDL_BUF_SIZE) 63 | 64 | if not chunk: 65 | break 66 | checksum.update(chunk) 67 | size += len(chunk) 68 | if len(chunk) < PDL_BUF_SIZE: 69 | break 70 | 71 | print(f"Received {size} bytes, sha512: {checksum.hexdigest()}") 72 | client_socket.close() 73 | 74 | def stop(self): 75 | """Stop the server.""" 76 | self._stopping = True 77 | closing_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 78 | closing_socket.connect((PRINTER_IP, PDL_DATASTREAM_PORT)) 79 | closing_socket.close() 80 | self._server_socket.close() 81 | 82 | 83 | def publish_mdns(ip_address: str, name: str) -> None: 84 | """ 85 | Publish the MDNS entry. 86 | 87 | The entry is published as long as the program runs. 88 | """ 89 | printer_uuid = uuid.uuid4() 90 | zeroconf = Zeroconf(interfaces=InterfaceChoice.All) 91 | 92 | ip_address_encoded = socket.inet_aton(ip_address) 93 | 94 | ws_info = ServiceInfo( 95 | type_="_pdl-datastream._tcp.local.", 96 | name=f"{name}._pdl-datastream._tcp.local.", 97 | port=PDL_DATASTREAM_PORT, 98 | # weight: int = 0, 99 | # priority: int = 0, 100 | properties={ 101 | b"product": name.encode("utf-8"), 102 | b"note": b"Printer", 103 | b"UUID": str(printer_uuid).encode("utf-8"), 104 | }, 105 | addresses=[ip_address_encoded], 106 | # server: Optional[str] = None, 107 | # host_ttl: int = 120, 108 | # other_ttl: int = 4500, 109 | # *, 110 | # addresses: Optional[List[bytes]] = None, 111 | # parsed_addresses: Optional[List[str]] = None, 112 | # interface_index: Optional[int] = None, 113 | ) 114 | zeroconf.register_service(ws_info) 115 | 116 | 117 | def run_snmpsim() -> None: 118 | """Run snmpsim as if called from command line.""" 119 | # Arguments for snmpsim-command-responder 120 | # Same as 121 | # uv run snmpsim-command-responder --data-dir=./data/ --agent-udpv4-endpoint=127.0.0.1:1024 122 | sys.argv.extend( 123 | [ 124 | "--data-dir=./data/", 125 | f"--agent-udpv4-endpoint={PRINTER_IP}:{SNMP_PORT}", 126 | # "--log-level=debug", 127 | # "--debug=all", 128 | ] 129 | ) 130 | snmpsim_main() 131 | 132 | 133 | def main() -> None: 134 | """Publish MDNS entry via zeroconf and run snmpsim.""" 135 | print("Publishing MDNS entry...") 136 | 137 | def publish_mdns_delayed() -> None: 138 | time.sleep(10) 139 | print("Publishing delayed dummy MDNS entry...") 140 | publish_mdns("1.2.3.4", "DUMMY") 141 | 142 | publish_mdns(PRINTER_IP, "MFC-9332CDW") 143 | delayed_mdns_thread = threading.Thread(target=publish_mdns_delayed) 144 | delayed_mdns_thread.start() 145 | 146 | print("Starting PDL Datastream Server...") 147 | pdl_ds_stream_thread = PdlDsStreamServer() 148 | pdl_ds_stream_thread.start() 149 | 150 | print("Starting SNMP Server...") 151 | run_snmpsim() 152 | 153 | pdl_ds_stream_thread.stop() 154 | delayed_mdns_thread.join() 155 | pdl_ds_stream_thread.join() 156 | 157 | 158 | if __name__ == "__main__": 159 | main() 160 | -------------------------------------------------------------------------------- /brother_printer_fwupd/snmp_info.py: -------------------------------------------------------------------------------- 1 | """Receive printer info via SNMP.""" 2 | 3 | import argparse 4 | import asyncio # pylint: disable=import-outside-toplevel 5 | import ipaddress 6 | import logging 7 | import re 8 | import sys 9 | import typing 10 | 11 | from pysnmp.hlapi.v3arch.asyncio import ( 12 | CommunityData, 13 | ContextData, 14 | ObjectIdentity, 15 | ObjectType, 16 | SnmpEngine, 17 | Udp6TransportTarget, 18 | UdpTransportTarget, 19 | bulk_cmd, 20 | is_end_of_mib, 21 | ) 22 | 23 | from .common import common_args 24 | from .models import FWInfo, IPAddress, SNMPPrinterInfo 25 | from .utils import CONSOLE_LOG_HANDLER, DEFAULT_SNMP_PORT, LOGGER, get_default_port 26 | 27 | SNMP_ROOT_OID = "1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2" 28 | 29 | SNMP_RE = re.compile(r'(?P[A-Z]+) ?= ?"(?P.*)"') 30 | 31 | 32 | async def snmp_walk( 33 | target: IPAddress, 34 | community: str = "public", 35 | port: int = DEFAULT_SNMP_PORT, 36 | root_oid: str = SNMP_ROOT_OID, 37 | ) -> typing.AsyncGenerator[str, None]: 38 | """Do a SNMP walk over a MIB given by it's start OID.""" 39 | 40 | if isinstance(target, str): 41 | target = ipaddress.ip_address(target) 42 | 43 | if isinstance(target, ipaddress.IPv6Address): 44 | udp_target = await Udp6TransportTarget.create((str(target), port)) 45 | elif isinstance(target, ipaddress.IPv4Address): 46 | udp_target = await UdpTransportTarget.create((str(target), port)) 47 | else: 48 | assert False 49 | 50 | engine = SnmpEngine() 51 | var_binds = [ObjectType(ObjectIdentity(root_oid))] 52 | 53 | while True: 54 | error_indication, error_status, error_index, var_bind_table = await bulk_cmd( 55 | engine, 56 | CommunityData(community, mpModel=0), 57 | udp_target, 58 | ContextData(), 59 | 0, 60 | 50, 61 | *var_binds, 62 | ) 63 | 64 | if error_indication: 65 | LOGGER.critical("%s", error_indication) 66 | sys.exit(1) 67 | elif error_status: 68 | if is_end_of_mib(var_bind_table): 69 | break 70 | LOGGER.critical( 71 | "%s at %s", 72 | error_status.prettyPrint(), 73 | var_binds[int(error_index) - 1][0] if error_index else "?", 74 | ) 75 | else: 76 | for var_bind in var_bind_table: 77 | LOGGER.debug(" = ".join([x.prettyPrint() for x in var_bind])) 78 | # TODO this is ugly 79 | payload = str(var_bind[1]).strip() 80 | 81 | if not payload: 82 | break 83 | 84 | yield payload 85 | 86 | var_binds = var_bind_table 87 | 88 | if is_end_of_mib(var_binds): 89 | break 90 | 91 | 92 | async def get_snmp_info( 93 | target: IPAddress, 94 | community: str = "public", 95 | port: int = DEFAULT_SNMP_PORT, 96 | ) -> SNMPPrinterInfo: 97 | """ 98 | Get the required info about the printer via SNMP. 99 | 100 | Equals to: 101 | snmpwalk -v 2c -c public lp.local 1.3.6.1.4.1.2435.2.4.3.99.3.1.6.1.2 102 | :return: A tuple of: 103 | - the model / series 104 | - the spec 105 | - a list of firmware infos, which are tuples of the id and their version. 106 | (Whatever this information means). 107 | """ 108 | printer_info = SNMPPrinterInfo() 109 | firm_id: typing.Optional[str] = None 110 | firm_ver: typing.Optional[str] = None 111 | 112 | async for payload in snmp_walk(target, community, port, SNMP_ROOT_OID): 113 | match = SNMP_RE.match(payload) 114 | 115 | if not match: 116 | LOGGER.critical('Payload "%s" does not match the regex.', payload) 117 | 118 | continue 119 | # sys.exit(1) 120 | name = match.group("name") 121 | value = match.group("value") 122 | 123 | if name == "MODEL": 124 | printer_info.model = value 125 | elif name == "SERIAL": 126 | printer_info.serial = value 127 | elif name == "SPEC": 128 | printer_info.spec = value 129 | elif name in ("FIRMID", "FIRMVER"): 130 | if name == "FIRMID": 131 | firm_id = value 132 | elif name == "FIRMVER": 133 | firm_ver = value 134 | 135 | if firm_id is not None and firm_ver is not None: 136 | printer_info.fw_versions.append(FWInfo(firmid=firm_id, firmver=firm_ver)) 137 | firm_id = None 138 | firm_ver = None 139 | else: 140 | LOGGER.debug("Ignoring SNMP info %s=%s", name, value) 141 | 142 | if firm_id is not None or firm_ver is not None: 143 | LOGGER.critical( 144 | "Did not receive firmid or firmver from printer via SNMP: firm_id=%s, firm_ver=%s", 145 | firm_id, 146 | firm_ver, 147 | ) 148 | sys.exit(1) 149 | 150 | return printer_info 151 | 152 | 153 | def get_snmp_info_sync( 154 | target: IPAddress, 155 | community: str = "public", 156 | port: int = DEFAULT_SNMP_PORT, 157 | ) -> SNMPPrinterInfo: 158 | """Synchoronous version of get_snmp_info.""" 159 | return asyncio.run(get_snmp_info(target=target, community=community, port=port)) 160 | 161 | 162 | def snmp_args(parser: argparse.ArgumentParser) -> None: 163 | """Add command line arguments for SNMP to an argparse parser.""" 164 | parser.add_argument( 165 | "-c", 166 | "--community", 167 | dest="community", 168 | default="public", 169 | help="SNMP Community string for the printer (default: '%(default)s').", 170 | ) 171 | parser.add_argument( 172 | "--snmp-port", 173 | dest="snmp_port", 174 | metavar="port", 175 | type=int, 176 | default=get_default_port("snmp"), 177 | help="The UDP port for SNMP at the printer (default: %(default)s).", 178 | ) 179 | 180 | 181 | def parse_args(): 182 | """Parse command line arguments.""" 183 | parser = argparse.ArgumentParser( 184 | description=__doc__.strip().splitlines()[0], 185 | ) 186 | 187 | common_args(parser, ip_required=True) 188 | snmp_args(parser) 189 | 190 | return parser.parse_args() 191 | 192 | 193 | def main() -> None: 194 | """Run the SNMP info module.""" 195 | 196 | args = parse_args() 197 | 198 | CONSOLE_LOG_HANDLER.setLevel(logging.DEBUG if args.debug else logging.INFO) 199 | 200 | result = get_snmp_info_sync( 201 | target=args.ip, 202 | community=args.community, 203 | port=args.snmp_port, 204 | ) 205 | LOGGER.success("%s", result) 206 | -------------------------------------------------------------------------------- /brother_printer_fwupd/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script to update the firmware of some Brother printers (e. g. MFC). 3 | """ 4 | 5 | import argparse 6 | import logging 7 | import sys 8 | import typing 9 | import webbrowser 10 | 11 | import termcolor 12 | 13 | from . import ISSUE_URL 14 | from .common import common_args, printer_info_args 15 | from .firmware_downloader import fw_downloader_args, get_info_and_download_fw 16 | from .firmware_uploader import fw_uploader_args, upload_fw 17 | from .models import SNMPPrinterInfo 18 | from .snmp_info import get_snmp_info_sync, snmp_args 19 | from .utils import CONSOLE_LOG_HANDLER, LOGGER, GitHubIssueReporter 20 | 21 | try: 22 | from .autodiscovery import PrinterDiscoverer 23 | 24 | PRINTER_DISCOVERER: typing.Optional[typing.Type[PrinterDiscoverer]] = PrinterDiscoverer 25 | except ImportError: 26 | PRINTER_DISCOVERER = None 27 | 28 | if typing.TYPE_CHECKING: 29 | from .models import IPAddress 30 | 31 | 32 | def main(): 33 | """Run the program.""" 34 | 35 | def handler_cb(report_url: str): 36 | LOGGER.error("This might be a bug.") 37 | 38 | while True: 39 | ret = ( 40 | input( 41 | termcolor.colored( 42 | "Do you want to open an issue on Github? [yN] ", color="yellow" 43 | ) 44 | ) 45 | .strip() 46 | .lower() 47 | ) 48 | 49 | if ret.lower() == "y": 50 | webbrowser.open(report_url) 51 | 52 | return 53 | elif ret.lower() == "n" or not ret: 54 | return 55 | 56 | try: 57 | with GitHubIssueReporter( 58 | logger=LOGGER, 59 | issue_url=ISSUE_URL, 60 | handler_cb=handler_cb, 61 | ) as issue_reporter: 62 | run(issue_reporter) 63 | except KeyboardInterrupt: 64 | print() 65 | LOGGER.critical("Quit") 66 | sys.exit(0) 67 | 68 | 69 | def run(issue_reporter: GitHubIssueReporter): 70 | """Do a firmware upgrade.""" 71 | args = parse_args() 72 | 73 | issue_reporter.set_context_data("--community", args.community) 74 | issue_reporter.set_context_data("--fw-dir", str(args.fw_dir)) 75 | issue_reporter.set_context_data("--os", args.os) 76 | issue_reporter.set_context_data("--download-only", args.download_only) 77 | issue_reporter.set_context_data("--debug", args.debug) 78 | 79 | CONSOLE_LOG_HANDLER.setLevel(logging.DEBUG if args.debug else logging.INFO) 80 | 81 | printer_ip: "typing.Optional[IPAddress]" = args.ip 82 | upload_port: typing.Optional[int] = args.pdl_ds_port 83 | use_snmp = not args.model or not args.serial or not args.spec or not args.fw_versions 84 | printer_ip_required = use_snmp or not args.download_only 85 | 86 | if not printer_ip and printer_ip_required: 87 | LOGGER.info("Discovering printer via MDNS.") 88 | assert PRINTER_DISCOVERER 89 | discoverer = PRINTER_DISCOVERER() 90 | mdns_printer_info = discoverer.run_cli() 91 | 92 | if mdns_printer_info: 93 | printer_ip = mdns_printer_info.ip_addr 94 | if not upload_port: 95 | upload_port = mdns_printer_info.port 96 | 97 | if not printer_ip and printer_ip_required: 98 | LOGGER.critical("No printer given or found.") 99 | sys.exit(1) 100 | 101 | if printer_ip: 102 | issue_reporter.set_context_data("--printer", str(printer_ip)) 103 | 104 | if use_snmp: 105 | LOGGER.info("Querying printer info via SNMP.") 106 | assert printer_ip, "Printer IP is required but not given." 107 | printer_info: SNMPPrinterInfo = get_snmp_info_sync( 108 | target=printer_ip, community=args.community, port=args.snmp_port 109 | ) 110 | else: 111 | printer_info = SNMPPrinterInfo.from_args(args) 112 | 113 | if printer_info.model: 114 | issue_reporter.set_context_data("--model", printer_info.model) 115 | 116 | if printer_info.serial: 117 | issue_reporter.set_context_data("--serial", printer_info.serial) 118 | 119 | if printer_info.spec: 120 | issue_reporter.set_context_data("--spec", printer_info.spec) 121 | 122 | if printer_info.fw_versions: 123 | issue_reporter.set_context_data( 124 | "--fw-versions", 125 | [str(fw_version) for fw_version in printer_info.fw_versions], 126 | ) 127 | 128 | versions_str = ", ".join(str(fw_info) for fw_info in printer_info.fw_versions) 129 | LOGGER.success( 130 | "%s %s with following firmware version(s): %s", 131 | "Detected" if use_snmp else "Using", 132 | printer_info.model, 133 | versions_str, 134 | ) 135 | LOGGER.info("Querying firmware download URL from Brother update API.") 136 | 137 | for fw_part in printer_info.fw_versions: 138 | fw_file_path = get_info_and_download_fw( 139 | printer_info=printer_info, 140 | fw_part=fw_part, 141 | os=args.os, 142 | fw_dir=args.fw_dir, 143 | ) 144 | 145 | if not fw_file_path: 146 | continue 147 | 148 | if args.download_only: 149 | LOGGER.info("Skipping firmware upload due to --download-only") 150 | else: 151 | assert printer_ip, "Printer IP is required but not given" 152 | try: 153 | upload_fw( 154 | target=printer_ip, 155 | port=args.pdl_ds_port, 156 | fw_file_path=fw_file_path, 157 | ) 158 | except OSError as err: 159 | LOGGER.error( 160 | "Could not upload firmware %s to update part %s: %s", 161 | fw_file_path, 162 | fw_part, 163 | str(err), 164 | ) 165 | 166 | continue 167 | 168 | input("Continue? ") 169 | 170 | LOGGER.success("Done.") 171 | 172 | 173 | def parse_args() -> argparse.Namespace: 174 | """Parse command line arguments.""" 175 | parser = argparse.ArgumentParser( 176 | description=__doc__.strip().splitlines()[0], 177 | ) 178 | 179 | common_args(parser, ip_required=not PRINTER_DISCOVERER) 180 | snmp_args(parser) 181 | printer_info_args(parser) 182 | fw_downloader_args(parser) 183 | fw_uploader_args(parser, set_pdl_ds_port_default=not PRINTER_DISCOVERER) 184 | 185 | parser.add_argument( 186 | "--download-only", 187 | dest="download_only", 188 | action="store_true", 189 | help=( 190 | "Do no install update but download firmware and save it" 191 | " under the directory path given with --fw-dir." 192 | ), 193 | ) 194 | 195 | return parser.parse_args() 196 | 197 | 198 | if __name__ == "__main__": 199 | main() 200 | -------------------------------------------------------------------------------- /brother_printer_fwupd/autodiscovery.py: -------------------------------------------------------------------------------- 1 | """Auto detect printer using zeroconf.""" 2 | 3 | # pylint: disable=C0103 4 | # pylint: disable=R1723 5 | 6 | import argparse 7 | import ipaddress 8 | import logging 9 | import typing 10 | from typing import Optional 11 | 12 | import termcolor 13 | import zeroconf 14 | 15 | from .common import common_args 16 | from .models import MDNSPrinterInfo 17 | from .utils import CONSOLE_LOG_HANDLER, LOGGER, clear_screen 18 | 19 | ZEROCONF_SERVICE_DOMAIN = "_pdl-datastream._tcp.local." 20 | 21 | termcolor.ATTRIBUTES["italic"] = 3 # type: ignore 22 | 23 | 24 | class PrinterDiscoverer(zeroconf.ServiceListener): 25 | """Discoverer of printers.""" 26 | 27 | def __init__(self) -> None: 28 | self._printers: list[MDNSPrinterInfo] = [] 29 | self._zc = zeroconf.Zeroconf() 30 | self._invalid_answer = False 31 | self._browser: Optional[zeroconf.ServiceBrowser] = None 32 | 33 | def remove_service(self, zc: zeroconf.Zeroconf, type_: str, name: str): 34 | """Called when a service disappears.""" 35 | LOGGER.debug("Service %s removed", name) 36 | self._remove_printer_infos_by_name(name) 37 | self._update_screen() 38 | 39 | @staticmethod 40 | def _zc_info_to_mdns_printer_infos( 41 | service_info: zeroconf.ServiceInfo, 42 | name: str, 43 | ) -> typing.Iterator[MDNSPrinterInfo]: 44 | """Convert the info from zeroconf into MDNSPrinterInfo instances.""" 45 | 46 | for addr in service_info.addresses: 47 | try: 48 | ip_addr = ipaddress.ip_address(addr) 49 | except ValueError as err: 50 | LOGGER.critical(err) 51 | 52 | return 53 | 54 | product_raw = service_info.properties.get(b"product", None) 55 | 56 | if product_raw: 57 | product = product_raw.decode("utf8") 58 | else: 59 | product = None 60 | 61 | note_raw = service_info.properties.get(b"note", None) 62 | 63 | if note_raw: 64 | note = note_raw.decode("utf8") 65 | else: 66 | note = None 67 | 68 | uuid_raw = service_info.properties.get(b"UUID", None) 69 | 70 | if uuid_raw: 71 | uuid = uuid_raw.decode("utf8") 72 | else: 73 | uuid = None 74 | 75 | yield MDNSPrinterInfo( 76 | ip_addr=ip_addr, 77 | name=name, 78 | port=service_info.port, 79 | product=product, 80 | note=note, 81 | uuid=uuid, 82 | ) 83 | 84 | def _remove_printer_infos_by_name(self, name: str): 85 | """Remove all known printer infos by their name.""" 86 | printers_to_remove = [p for p in self._printers if p.name == name] 87 | 88 | for printer in printers_to_remove: 89 | self._printers.remove(printer) 90 | 91 | def _add_printer_infos(self, zc: zeroconf.Zeroconf, type_: str, name: str): 92 | """Add printer info.""" 93 | service_info = zc.get_service_info(type_, name) 94 | 95 | if not service_info: 96 | LOGGER.error( 97 | "Received empty add_service request. Ignoring: %s %s", 98 | type_, 99 | name, 100 | ) 101 | 102 | return 103 | 104 | for printer_info in PrinterDiscoverer._zc_info_to_mdns_printer_infos(service_info, name): 105 | self._printers.append(printer_info) 106 | 107 | self._update_screen() 108 | 109 | def add_service(self, zc: zeroconf.Zeroconf, type_: str, name: str): 110 | """Called, when a new service appears.""" 111 | LOGGER.debug("Service %s added", name) 112 | self._add_printer_infos(zc, type_, name) 113 | 114 | def update_service(self, zc: zeroconf.Zeroconf, type_: str, name: str): 115 | """Update a service.""" 116 | LOGGER.debug("Service {name} updated") 117 | self._remove_printer_infos_by_name(name) 118 | self._add_printer_infos(zc, type_, name) 119 | 120 | def _update_screen(self): 121 | """Update the CLI printer selection screen.""" 122 | clear_screen() 123 | 124 | termcolor.cprint("Choose a printer", attrs=["bold"], end=" ") 125 | termcolor.cprint("Scanning Network via MDNS...", attrs=["italic"]) 126 | 127 | if self._invalid_answer: 128 | LOGGER.error("Invalid answer.") 129 | print() 130 | 131 | if self._printers: 132 | max_str_len = len(str(len(self._printers) - 1)) 133 | max_ip_len = max(len(str(info.ip_addr)) for info in self._printers) 134 | 135 | for i, info in enumerate(self._printers): 136 | num_str = termcolor.colored( 137 | f"[{str(i).rjust(max_str_len)}]", color="blue", attrs=["bold"] 138 | ) 139 | ip_addr_str = termcolor.colored(str(info.ip_addr).rjust(max_ip_len), color="yellow") 140 | port_str = termcolor.colored(f"Port {info.port}") 141 | name_str = termcolor.colored(info.name, color="white") 142 | product_str = ( 143 | termcolor.colored(f"- Product: {info.product}") if info.product else "" 144 | ) 145 | note_str = ( 146 | termcolor.colored(f"- Note: {info.note}", attrs=["italic"]) if info.note else "" 147 | ) 148 | uuid_str = ( 149 | termcolor.colored(f"- UUID: {info.uuid}", attrs=["italic"]) if info.uuid else "" 150 | ) 151 | print( 152 | " ".join( 153 | ( 154 | num_str, 155 | ip_addr_str, 156 | port_str, 157 | name_str, 158 | product_str, 159 | note_str, 160 | uuid_str, 161 | ) 162 | ) 163 | ) 164 | 165 | print() 166 | 167 | if len(self._printers) > 1: 168 | range_str = f"[0 - {len(self._printers) - 1}; Enter: Cancel]" 169 | else: 170 | range_str = "[0 / Enter: Use first entry; ^C: Cancel]" 171 | 172 | range_str = termcolor.colored(range_str, color="blue") 173 | termcolor.cprint( 174 | f"Your choice {range_str}:", 175 | attrs=["bold"], 176 | end=" ", 177 | flush=True, 178 | ) 179 | else: 180 | termcolor.cprint("No printers found yet.", "yellow", attrs=["italic"], end=" ") 181 | termcolor.cprint( 182 | "Run with --printer= to skip autodiscovery. [Enter: Cancel]", 183 | attrs=["italic"], 184 | ) 185 | 186 | def run_cli(self) -> Optional[MDNSPrinterInfo]: 187 | """Run as interactive terminal application.""" 188 | self._run() 189 | self._update_screen() 190 | 191 | try: 192 | while True: 193 | inpt = input() 194 | 195 | if not inpt.strip(): 196 | # Enter 197 | if len(self._printers) == 1: 198 | return self._printers[0] 199 | else: 200 | return None 201 | 202 | try: 203 | return self._printers[int(inpt)] 204 | except (ValueError, IndexError): 205 | self._invalid_answer = True 206 | self._update_screen() 207 | except (KeyboardInterrupt, EOFError): 208 | print() 209 | return None 210 | finally: 211 | self._stop() 212 | clear_screen() 213 | 214 | def _run(self): 215 | """Auto detect printer using zeroconf.""" 216 | self._browser = zeroconf.ServiceBrowser( 217 | zc=self._zc, 218 | type_=ZEROCONF_SERVICE_DOMAIN, 219 | handlers=self, 220 | ) 221 | 222 | def _stop(self): 223 | """Stop discovering.""" 224 | self._zc.close() 225 | 226 | 227 | def parse_args() -> argparse.Namespace: 228 | """Parse command line arguments.""" 229 | parser = argparse.ArgumentParser( 230 | description=__doc__.strip().splitlines()[0], 231 | ) 232 | 233 | common_args(parser, ip_required=False) 234 | 235 | return parser.parse_args() 236 | 237 | 238 | def main() -> None: 239 | """Run the autodiscoverer module.""" 240 | args = parse_args() 241 | CONSOLE_LOG_HANDLER.setLevel(logging.DEBUG if args.debug else logging.INFO) 242 | 243 | discoverer = PrinterDiscoverer() 244 | mdns_printer_info = discoverer.run_cli() 245 | LOGGER.success("Found %s", mdns_printer_info) 246 | -------------------------------------------------------------------------------- /brother_printer_fwupd/utils.py: -------------------------------------------------------------------------------- 1 | """Some utilities used in other modules.""" 2 | 3 | # pylint: disable=R1705 4 | 5 | import io 6 | import logging 7 | import os 8 | import shlex 9 | import socket 10 | import string 11 | import sys 12 | import traceback 13 | import typing 14 | from pathlib import Path 15 | from urllib.parse import urlencode 16 | 17 | import termcolor 18 | 19 | 20 | class GitHubIssueReporter: 21 | """Wrapper around code that helps with reporting issues to GitHub.""" 22 | 23 | def __init__( 24 | self, 25 | logger: logging.Logger, 26 | issue_url: str, 27 | handler_cb: typing.Callable[[str], None], 28 | ): 29 | self.logger = logger 30 | self.issue_url = issue_url 31 | self.handler_cb = handler_cb 32 | self._handler = logging.StreamHandler(stream=io.StringIO()) 33 | self._handler.setLevel(logging.DEBUG) 34 | 35 | self._context: dict[str, typing.Union[str, bool, list[str]]] = {} 36 | 37 | def __enter__(self): 38 | self._handler.stream.seek(0) 39 | self._handler.stream.truncate() 40 | self.logger.addHandler(self._handler) 41 | 42 | return self 43 | 44 | def __exit__(self, exc_class, exc, tb): 45 | if not exc_class or exc_class in (SystemExit, KeyboardInterrupt): 46 | return 47 | 48 | if isinstance(exc, ExceptionGroupCompat): 49 | LOGGER.error("%s Errors:", exc.message) 50 | 51 | for err in exc.exceptions: 52 | LOGGER.error(" - %s", err) 53 | else: 54 | LOGGER.error(exc) 55 | self.logger.removeHandler(self._handler) 56 | self._handler.stream.seek(0) 57 | exc_io = io.StringIO() 58 | traceback.print_exception(exc, file=exc_io) 59 | exc_io.seek(0) 60 | report_url = self._generate_github_issue_url( 61 | title=str(exc), 62 | log_output=self._handler.stream.read(), 63 | exception_traceback=exc_io.read(), 64 | ) 65 | self.handler_cb(report_url) 66 | sys.exit(1) 67 | 68 | def _generate_github_issue_url( 69 | self, title: str, log_output: str, exception_traceback: str 70 | ) -> str: 71 | """Generate the URL to open an issue on GitLab.""" 72 | prog = Path(sys.argv[0]).name 73 | cmd = f"{prog} {shlex.join(sys.argv[1:])}" 74 | 75 | emulation_cmd_parts = [prog] 76 | 77 | for key, value in self._context.items(): 78 | if isinstance(value, bool): 79 | if value is True: 80 | emulation_cmd_parts.append(key) 81 | elif isinstance(value, list): 82 | emulation_cmd_parts.extend([key, *value]) 83 | else: 84 | emulation_cmd_parts.extend([key, value]) 85 | 86 | emulation_cmd = shlex.join(emulation_cmd_parts) 87 | emulation_block = ( 88 | "" 89 | if not self._context 90 | else f""" 91 | **Command to emulate scenario:** 92 | 93 | ```sh 94 | {emulation_cmd} 95 | ``` 96 | 97 | """ 98 | ) 99 | 100 | return ( 101 | self.issue_url 102 | + "?" 103 | + urlencode( 104 | { 105 | "title": title, 106 | "body": f""" 107 | **Description:** 108 | 109 | *please describe the issue* 110 | 111 | **Command:** 112 | 113 | ```sh 114 | {cmd} 115 | ``` 116 | {emulation_block} 117 | **Output:** 118 | 119 | ``` 120 | {log_output} 121 | ``` 122 | 123 | **Exception:** 124 | 125 | ```python 126 | {exception_traceback} 127 | ``` 128 | """.strip(), 129 | } 130 | ) 131 | ) 132 | 133 | def set_context_data(self, key: str, value: typing.Union[str, bool, list[str]]): 134 | """ 135 | Add some context info that helps crafting a command, which simulates the current scenario. 136 | 137 | Autodetection of most information can be bypassed by using command line arguments. 138 | """ 139 | self._context[key] = value 140 | 141 | 142 | def get_running_os() -> ( 143 | typing.Union[typing.Literal["WINDOWS"], typing.Literal["MAC"], typing.Literal["LINUX"]] 144 | ): 145 | """:return: "WINDOWS", "MAC", or "LINUX" according to the currently running OS.""" 146 | 147 | if sys.platform.startswith("win") or sys.platform.startswith("cygwin"): 148 | return "WINDOWS" 149 | elif sys.platform.startswith("darwin"): 150 | return "MAC" 151 | else: 152 | return "LINUX" 153 | 154 | 155 | def add_logging_level(level_name: str, level_num: int, method_name: typing.Optional[str] = None): 156 | """ 157 | Comprehensively adds a new logging level to the `logging` module and the 158 | currently configured logging class. 159 | 160 | `level_name` becomes an attribute of the `logging` module with the value 161 | `level_num`. `method_name` becomes a convenience method for both `logging` 162 | itself and the class returned by `logging.getLoggerClass()` (usually just 163 | `logging.Logger`). If `method_name` is not specified, `level_name.lower()` is 164 | used. 165 | 166 | To avoid accidental clobberings of existing attributes, this method will 167 | raise an `AttributeError` if the level name is already an attribute of the 168 | `logging` module or if the method name is already present 169 | 170 | Example 171 | ------- 172 | >>> add_logging_level('TRACE', logging.DEBUG - 5) 173 | >>> logging.getLogger(__name__).setLevel("TRACE") 174 | >>> logging.getLogger(__name__).trace('that worked') 175 | >>> logging.trace('so did this') 176 | >>> logging.TRACE 177 | 5 178 | 179 | """ 180 | 181 | if not method_name: 182 | method_name = level_name.lower() 183 | 184 | if hasattr(logging, level_name): 185 | raise AttributeError(f"{level_name} already defined in logging module") 186 | 187 | if hasattr(logging, method_name): 188 | raise AttributeError(f"{method_name} already defined in logging module") 189 | 190 | if hasattr(logging.getLoggerClass(), method_name): 191 | raise AttributeError(f"{method_name} already defined in logger class") 192 | 193 | # This method was inspired by the answers to Stack Overflow post 194 | # http://stackoverflow.com/q/2183233/2988730, especially 195 | # http://stackoverflow.com/a/13638084/2988730 196 | def log_for_level(self, message, *args, **kwargs): 197 | if self.isEnabledFor(level_num): 198 | self._log( # pylint: disable=protected-access 199 | level_num, message, args, **kwargs 200 | ) 201 | 202 | def log_to_root(message, *args, **kwargs): 203 | logging.log(level_num, message, *args, **kwargs) 204 | 205 | logging.addLevelName(level_num, level_name) 206 | setattr(logging, level_name, level_num) 207 | setattr(logging.getLoggerClass(), method_name, log_for_level) 208 | setattr(logging, method_name, log_to_root) 209 | 210 | 211 | add_logging_level("SUCCESS", logging.INFO + 5) 212 | 213 | 214 | class TerminalFormatter(logging.Formatter): 215 | """Logging formatter with colors.""" 216 | 217 | colors = { 218 | logging.DEBUG: "grey", 219 | logging.INFO: "cyan", 220 | logging.SUCCESS: "green", # type: ignore # pylint: disable=no-member 221 | logging.WARNING: "yellow", 222 | logging.ERROR: "red", 223 | logging.CRITICAL: "red", 224 | } 225 | prefix = { 226 | logging.DEBUG: "[d]", 227 | logging.INFO: "[i]", 228 | logging.SUCCESS: "[i]", # type: ignore # pylint: disable=no-member 229 | logging.WARNING: "[!]", 230 | logging.ERROR: "[!]", 231 | logging.CRITICAL: "[!]", 232 | } 233 | attrs = { 234 | logging.CRITICAL: ["bold"], 235 | } 236 | 237 | def __init__(self, fmt="%(message)s"): 238 | super().__init__(fmt, datefmt="%Y-%m-%d %H:%M:%S") 239 | 240 | def format(self, record): 241 | return termcolor.colored( 242 | f"{self.prefix[record.levelno]} {super().format(record)}", 243 | color=self.colors[record.levelno], 244 | attrs=self.attrs.get(record.levelno), 245 | ) 246 | 247 | 248 | class LoggerWithSuccess(logging.Logger): 249 | """A logger with success() method.""" 250 | 251 | def success(self, msg: str, *args, **kwargs) -> None: 252 | """Log a success message.""" 253 | 254 | 255 | CONSOLE_LOG_HANDLER = logging.StreamHandler(stream=sys.stderr) 256 | CONSOLE_LOG_HANDLER.setFormatter(TerminalFormatter()) 257 | LOGGER = typing.cast(LoggerWithSuccess, logging.getLogger(name="brother_printer_fwupd")) 258 | LOGGER.setLevel(logging.DEBUG) 259 | LOGGER.addHandler(CONSOLE_LOG_HANDLER) 260 | 261 | 262 | def clear_screen(): 263 | """Clear the terminal screen.""" 264 | os.system("clear") 265 | # print(chr(27) + '[2j') 266 | # print('\033c') 267 | # print('\x1bc') 268 | 269 | 270 | def sluggify(value: str) -> str: 271 | """Convert value to a string that can be safely used as file name.""" 272 | trans_tab = { 273 | " ": "_", 274 | "@": "-", 275 | ":": "-", 276 | } 277 | trans = str.maketrans(trans_tab) 278 | allowed_chars = string.ascii_letters + string.digits + "".join(trans_tab.values()) 279 | assert "/" not in allowed_chars 280 | assert "." not in allowed_chars 281 | value = value.strip().lower().translate(trans) 282 | value = "".join(c for c in value if c in allowed_chars) 283 | 284 | return value 285 | 286 | 287 | #: The default port for SNMP (UDP) 288 | DEFAULT_SNMP_PORT = 161 289 | 290 | #: The default port for PDL Datastream / jetdirect (TCP) 291 | DEFAULT_PDL_DATASTREAM_PORT = 9100 292 | 293 | #: A mapping of service names (according to /etc/services) to port numbers. 294 | DEFAULT_PORTS = { 295 | "snmp": DEFAULT_SNMP_PORT, 296 | "pdl-datastream": DEFAULT_PDL_DATASTREAM_PORT, 297 | } 298 | 299 | 300 | def get_default_port(name: str) -> int: 301 | """ 302 | Get the default port by service name by querying the system or use a known fallback. 303 | 304 | :raises: `KeyError` if the lookup on the system failed and the service is unknown. 305 | """ 306 | try: 307 | return socket.getservbyname(name) 308 | except OSError: 309 | return DEFAULT_PORTS[name] 310 | 311 | 312 | class ExceptionGroupCompat(BaseException): 313 | """Compatibility for ExceptionGroup (introduced in python 3.11.""" 314 | 315 | def __init__(self, msg: str, exceptions: list[Exception]): 316 | super().__init__(msg) 317 | self.message = msg 318 | self.exceptions = exceptions 319 | 320 | def __str__(self) -> str: 321 | return self.message + "\n - " + "\n - ".join(str(err) for err in self.exceptions) 322 | -------------------------------------------------------------------------------- /brother_printer_fwupd/firmware_downloader.py: -------------------------------------------------------------------------------- 1 | """Get the firmware download URL and download the firmare from the official Brother website.""" 2 | 3 | import argparse 4 | import logging 5 | import typing 6 | from copy import copy 7 | from pathlib import Path 8 | 9 | import requests 10 | from bs4 import BeautifulSoup, Tag 11 | 12 | from . import ISSUE_URL 13 | from .common import common_args, printer_info_args 14 | from .models import FWInfo, SNMPPrinterInfo 15 | from .utils import ( 16 | CONSOLE_LOG_HANDLER, 17 | LOGGER, 18 | ExceptionGroupCompat, 19 | get_running_os, 20 | sluggify, 21 | ) 22 | 23 | FW_UPDATE_URL = "https://firmverup.brother.co.jp/kne_bh7_update_nt_ssl/ifax2.asmx/fileUpdate" 24 | 25 | API_REQUEST_DATA_TEMPLATE = BeautifulSoup( 26 | """ 27 | 28 | 29 | 30 | 31 | 1 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 1 43 | 2 44 | 45 | 1 46 | 47 | 48 | """.strip(), 49 | "xml", 50 | ) 51 | 52 | REQUESTS_TIMEOUT = 10 53 | 54 | 55 | RUNNING_OS = get_running_os() 56 | 57 | 58 | def get_download_url( 59 | printer_info: "SNMPPrinterInfo", 60 | reported_os: str, 61 | firmid: str = "MAIN", 62 | ) -> typing.Union[tuple[str, str], tuple[None, None]]: 63 | """ 64 | Get the firmware download URL for the target printer. 65 | 66 | :return: Tuple of download latest version and URL. 67 | """ 68 | 69 | api_request_data = copy(API_REQUEST_DATA_TEMPLATE) 70 | api_request_data.REQUESTINFO.FIRMUPDATETOOLINFO.FIRMCATEGORY.string = firmid 71 | api_request_data.REQUESTINFO.FIRMUPDATETOOLINFO.OS.string = reported_os 72 | api_request_data.REQUESTINFO.FIRMUPDATEINFO.MODELINFO.NAME.string = printer_info.model 73 | api_request_data.REQUESTINFO.FIRMUPDATEINFO.MODELINFO.SPEC.string = printer_info.spec 74 | 75 | for fw_info in printer_info.fw_versions: 76 | firm_info = Tag(name="FIRM", parser="xml") 77 | firm_info.append(Tag(name="ID", parser="xml")) 78 | firm_info.append(Tag(name="VERSION", parser="xml")) 79 | firm_info.ID.string = fw_info.firmid 80 | firm_info.VERSION.string = fw_info.firmver 81 | 82 | api_request_data.REQUESTINFO.FIRMUPDATEINFO.MODELINFO.FIRMINFO.append(firm_info) 83 | 84 | errors: list[Exception] = [] 85 | 86 | for modification_callback in (copy, apply_driver_ews, apply_api_misspelling): 87 | api_request_data_str = str(modification_callback(api_request_data)) 88 | 89 | # curl -X POST -d @hl3040cn-update.xml -H "Content-Type:text/xml" 90 | LOGGER.debug( 91 | "Sending POST request to %s with following content:\n%s", 92 | FW_UPDATE_URL, 93 | api_request_data_str, 94 | ) 95 | resp = requests.post( 96 | FW_UPDATE_URL, 97 | data=api_request_data_str, 98 | headers={"Content-Type": "text/xml"}, 99 | timeout=REQUESTS_TIMEOUT, 100 | ) 101 | resp.raise_for_status() 102 | LOGGER.debug("Response:\n%s", resp.text) 103 | 104 | try: 105 | return parse_response(response=resp.text, firmid=firmid) 106 | except ValueError as err: 107 | errors.append(err) 108 | LOGGER.warning(err) 109 | continue 110 | 111 | raise ExceptionGroupCompat("Giving up fetching firmware.", errors) 112 | 113 | 114 | def parse_response(response: str, firmid: str) -> typing.Union[tuple[str, str], tuple[None, None]]: 115 | """ 116 | Parse the API response and return a tuple of the latest version and the download URL. 117 | """ 118 | resp_xml = BeautifulSoup(response, "xml") 119 | 120 | def select_one(name: str) -> str: 121 | tags = resp_xml.select(name) 122 | if len(tags) > 1: 123 | raise ValueError( 124 | f"Invalid response: Expected only one tag of name '{name}' in response '{resp_xml}'." 125 | ) 126 | elif len(tags) == 0: 127 | raise ValueError( 128 | f"Invalid response: Expected tag '{name}' to be in response '{resp_xml}'." 129 | ) 130 | return tags[0].text 131 | 132 | versioncheck_val = select_one("VERSIONCHECK") 133 | if versioncheck_val == "1": 134 | LOGGER.success("Firmware part %s seems to be up to date.", firmid) 135 | return None, None 136 | elif versioncheck_val == "0": 137 | # Firmware update required 138 | pass 139 | elif versioncheck_val == "2": 140 | LOGGER.error( 141 | ( 142 | "Received versioncheck value '2' for firmware part %s." 143 | " I'm sorry, but I don't know, what Brother wants to say with this code." 144 | " If you have any information, please open an issue on GitHub:" 145 | " %s" 146 | ), 147 | firmid, 148 | ISSUE_URL, 149 | ) 150 | return None, None 151 | else: 152 | raise ValueError( 153 | f"Unknown value of 'versioncheck' in response for firmid={firmid}: '{resp_xml}'." 154 | ) 155 | 156 | latest_version = select_one("LATESTVERSION") 157 | LOGGER.info("Update for firmware part '%s' available (version '%s')", firmid, latest_version) 158 | 159 | firmid_val = select_one("FIRMID") 160 | if firmid_val != firmid: 161 | LOGGER.warning( 162 | "Request for firmid=%s was answered with firmid=%s. Be careful!", 163 | firmid, 164 | firmid_val, 165 | ) 166 | 167 | return latest_version, select_one("PATH") 168 | 169 | 170 | def apply_driver_ews(data: BeautifulSoup) -> BeautifulSoup: 171 | """ 172 | Modify the request data in the way it is required for MFC-L3750CDW, HL-L2360DW and others. 173 | 174 | 1. Remove `` / `` 175 | 2. Add "EWS" to `` 176 | See https://github.com/sedrubal/brother_printer_fwupd/issues/19#issuecomment-2079813638 177 | """ 178 | LOGGER.info("Trying again without in API request but with EWS...") 179 | data = copy(data) 180 | data.REQUESTINFO.FIRMUPDATEINFO.MODELINFO.DRIVER.string = "EWS" 181 | data.REQUESTINFO.FIRMUPDATEINFO.MODELINFO.SERIALNO.replace_with( 182 | Tag(name="SELIALNO", parser="xml") 183 | ) 184 | return data 185 | 186 | 187 | def apply_api_misspelling(data: BeautifulSoup) -> BeautifulSoup: 188 | """ 189 | Another modification which works for MFC-L3750CDW, HL-L2360DW and others. 190 | 191 | 1. Replace `` with typo `` 192 | 2. Add "EWS" to `` 193 | See https://github.com/sedrubal/brother_printer_fwupd/issues/19 194 | """ 195 | LOGGER.info("Trying again with misspelling in API request...") 196 | data = copy(data) 197 | data.REQUESTINFO.FIRMUPDATEINFO.MODELINFO.DRIVER.string = "EWS" 198 | data.REQUESTINFO.FIRMUPDATEINFO.MODELINFO.SERIALNO.replace_with( 199 | Tag(name="SELIALNO", parser="xml") 200 | ) 201 | return data 202 | 203 | 204 | def download_fw( 205 | url: str, 206 | dst_dir: Path, 207 | printer_model: str, 208 | fw_part: typing.Union[str, FWInfo], 209 | latest_version: str, 210 | ): 211 | """Download the firmware.""" 212 | resp = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT) 213 | resp.raise_for_status() 214 | total_size = int(resp.headers.get("content-length", 0)) 215 | size_written = 0 216 | chunk_size = 8192 217 | 218 | out_file = dst_dir / (sluggify(f"firmware-{printer_model}-{fw_part}-{latest_version}") + ".djf") 219 | 220 | with out_file.open("wb") as out: 221 | for chunk in resp.iter_content(chunk_size): 222 | size_written += out.write(chunk) 223 | progress = size_written / total_size * 100 224 | print(f"\r{progress: 5.1f} %", end="", flush=True) 225 | 226 | print() 227 | return out_file 228 | 229 | 230 | def get_info_and_download_fw( 231 | printer_info, 232 | fw_part: FWInfo, 233 | os: str, 234 | fw_dir: Path, 235 | ) -> typing.Optional[Path]: 236 | """Try to get the update URL and download the new firmware.""" 237 | LOGGER.info("Try to get information for firmware part %s", fw_part) 238 | 239 | latest_version, download_url = get_download_url( 240 | printer_info=printer_info, 241 | firmid=str(fw_part.firmid), 242 | reported_os=os, 243 | ) 244 | 245 | if not download_url: 246 | return None 247 | 248 | assert latest_version 249 | assert download_url 250 | model = printer_info.model 251 | assert model 252 | 253 | LOGGER.debug(" Download URL is %s", download_url) 254 | LOGGER.success("Downloading firmware file.") 255 | fw_file_path = download_fw( 256 | url=download_url, 257 | dst_dir=fw_dir, 258 | printer_model=model, 259 | fw_part=fw_part, 260 | latest_version=latest_version, 261 | ) 262 | 263 | return fw_file_path 264 | 265 | 266 | def fw_downloader_args(parser: argparse.ArgumentParser) -> None: 267 | """Add command line arguments for the firmware downloader to an argparse parser.""" 268 | parser.add_argument( 269 | "--os", 270 | dest="os", 271 | type=str.upper, 272 | default=RUNNING_OS, 273 | choices=["WINDOWS", "MAC", "LINUX"], 274 | help="Operating system to report when downloading firmware (default: '%(default)s').", 275 | ) 276 | parser.add_argument( 277 | "-o", 278 | "--fw-dir", 279 | type=Path, 280 | dest="fw_dir", 281 | default=".", 282 | help="Directory, where the firmware will be downloaded (default: '%(default)s').", 283 | ) 284 | 285 | 286 | def parse_args() -> argparse.Namespace: 287 | """Parse command line arguments.""" 288 | parser = argparse.ArgumentParser( 289 | description=__doc__.strip().splitlines()[0], 290 | ) 291 | 292 | common_args(parser, ip_required=False) 293 | fw_downloader_args(parser) 294 | printer_info_args(parser, required=True) 295 | 296 | return parser.parse_args() 297 | 298 | 299 | def main() -> None: 300 | """Run the downloader.""" 301 | args = parse_args() 302 | 303 | CONSOLE_LOG_HANDLER.setLevel(logging.DEBUG if args.debug else logging.INFO) 304 | 305 | printer_info = SNMPPrinterInfo.from_args(args) 306 | 307 | for fw_part in printer_info.fw_versions: 308 | fw_file_path = get_info_and_download_fw( 309 | printer_info=printer_info, 310 | fw_part=fw_part, 311 | os=args.os, 312 | fw_dir=args.fw_dir, 313 | ) 314 | 315 | if fw_file_path: 316 | LOGGER.success("Downloaded firmware for %s to %s", fw_part.firmid, fw_file_path) 317 | 318 | 319 | if __name__ == "__main__": 320 | main() 321 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint 9 | # in a server-like mode. 10 | clear-cache-post-run=no 11 | 12 | # Load and enable all available extensions. Use --list-extensions to see a list 13 | # all available extensions. 14 | #enable-all-extensions= 15 | 16 | # In error mode, messages with a category besides ERROR or FATAL are 17 | # suppressed, and no reports are done by default. Error mode is compatible with 18 | # disabling specific errors. 19 | #errors-only= 20 | 21 | # Always return a 0 (non-error) status code, even if lint errors are found. 22 | # This is primarily useful in continuous integration scripts. 23 | #exit-zero= 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code. 28 | extension-pkg-allow-list= 29 | 30 | # A comma-separated list of package or module names from where C extensions may 31 | # be loaded. Extensions are loading into the active Python interpreter and may 32 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 33 | # for backward compatibility.) 34 | extension-pkg-whitelist= 35 | 36 | # Return non-zero exit code if any of these messages/categories are detected, 37 | # even if score is above --fail-under value. Syntax same as enable. Messages 38 | # specified are enabled, while categories only check already-enabled messages. 39 | fail-on= 40 | 41 | # Specify a score threshold under which the program will exit with error. 42 | fail-under=10 43 | 44 | # Interpret the stdin as a python script, whose filename needs to be passed as 45 | # the module_or_package argument. 46 | #from-stdin= 47 | 48 | # Files or directories to be skipped. They should be base names, not paths. 49 | ignore=CVS 50 | 51 | # Add files or directories matching the regular expressions patterns to the 52 | # ignore-list. The regex matches against paths and can be in Posix or Windows 53 | # format. Because '\\' represents the directory delimiter on Windows systems, 54 | # it can't be used as an escape character. 55 | ignore-paths= 56 | 57 | # Files or directories matching the regular expression patterns are skipped. 58 | # The regex matches against base names, not paths. The default value ignores 59 | # Emacs file locks 60 | ignore-patterns=^\.# 61 | 62 | # List of module names for which member attributes should not be checked and 63 | # will not be imported (useful for modules/projects where namespaces are 64 | # manipulated during runtime and thus existing member attributes cannot be 65 | # deduced by static analysis). It supports qualified module names, as well as 66 | # Unix pattern matching. 67 | ignored-modules= 68 | 69 | # Python code to execute, usually for sys.path manipulation such as 70 | # pygtk.require(). 71 | #init-hook= 72 | 73 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 74 | # number of processors available to use, and will cap the count on Windows to 75 | # avoid hangs. 76 | jobs=1 77 | 78 | # Control the amount of potential inferred values when inferring a single 79 | # object. This can help the performance when dealing with large functions or 80 | # complex, nested conditions. 81 | limit-inference-results=100 82 | 83 | # List of plugins (as comma separated values of python module names) to load, 84 | # usually to register additional checkers. 85 | load-plugins= 86 | 87 | # Pickle collected data for later comparisons. 88 | persistent=yes 89 | 90 | # Resolve imports to .pyi stubs if available. May reduce no-member messages and 91 | # increase not-an-iterable messages. 92 | prefer-stubs=no 93 | 94 | # Minimum Python version to use for version dependent checks. Will default to 95 | # the version used to run pylint. 96 | py-version=3.11 97 | 98 | # Discover python modules and packages in the file system subtree. 99 | recursive=no 100 | 101 | # Add paths to the list of the source roots. Supports globbing patterns. The 102 | # source root is an absolute path or a path relative to the current working 103 | # directory used to determine a package namespace for modules located under the 104 | # source root. 105 | source-roots= 106 | 107 | # When enabled, pylint would attempt to guess common misconfiguration and emit 108 | # user-friendly hints instead of false-positive error messages. 109 | suggestion-mode=yes 110 | 111 | # Allow loading of arbitrary C extensions. Extensions are imported into the 112 | # active Python interpreter and may run arbitrary code. 113 | unsafe-load-any-extension=no 114 | 115 | # In verbose mode, extra non-checker-related info will be displayed. 116 | #verbose= 117 | 118 | 119 | [BASIC] 120 | 121 | # Naming style matching correct argument names. 122 | argument-naming-style=snake_case 123 | 124 | # Regular expression matching correct argument names. Overrides argument- 125 | # naming-style. If left empty, argument names will be checked with the set 126 | # naming style. 127 | #argument-rgx= 128 | 129 | # Naming style matching correct attribute names. 130 | attr-naming-style=snake_case 131 | 132 | # Regular expression matching correct attribute names. Overrides attr-naming- 133 | # style. If left empty, attribute names will be checked with the set naming 134 | # style. 135 | #attr-rgx= 136 | 137 | # Bad variable names which should always be refused, separated by a comma. 138 | bad-names=foo, 139 | bar, 140 | baz, 141 | toto, 142 | tutu, 143 | tata 144 | 145 | # Bad variable names regexes, separated by a comma. If names match any regex, 146 | # they will always be refused 147 | bad-names-rgxs= 148 | 149 | # Naming style matching correct class attribute names. 150 | class-attribute-naming-style=any 151 | 152 | # Regular expression matching correct class attribute names. Overrides class- 153 | # attribute-naming-style. If left empty, class attribute names will be checked 154 | # with the set naming style. 155 | #class-attribute-rgx= 156 | 157 | # Naming style matching correct class constant names. 158 | class-const-naming-style=UPPER_CASE 159 | 160 | # Regular expression matching correct class constant names. Overrides class- 161 | # const-naming-style. If left empty, class constant names will be checked with 162 | # the set naming style. 163 | #class-const-rgx= 164 | 165 | # Naming style matching correct class names. 166 | class-naming-style=PascalCase 167 | 168 | # Regular expression matching correct class names. Overrides class-naming- 169 | # style. If left empty, class names will be checked with the set naming style. 170 | #class-rgx= 171 | 172 | # Naming style matching correct constant names. 173 | const-naming-style=UPPER_CASE 174 | 175 | # Regular expression matching correct constant names. Overrides const-naming- 176 | # style. If left empty, constant names will be checked with the set naming 177 | # style. 178 | #const-rgx= 179 | 180 | # Minimum line length for functions/classes that require docstrings, shorter 181 | # ones are exempt. 182 | docstring-min-length=-1 183 | 184 | # Naming style matching correct function names. 185 | function-naming-style=snake_case 186 | 187 | # Regular expression matching correct function names. Overrides function- 188 | # naming-style. If left empty, function names will be checked with the set 189 | # naming style. 190 | #function-rgx= 191 | 192 | # Good variable names which should always be accepted, separated by a comma. 193 | good-names=i, 194 | j, 195 | k, 196 | ex, 197 | Run, 198 | _ 199 | 200 | # Good variable names regexes, separated by a comma. If names match any regex, 201 | # they will always be accepted 202 | good-names-rgxs= 203 | 204 | # Include a hint for the correct naming format with invalid-name. 205 | include-naming-hint=no 206 | 207 | # Naming style matching correct inline iteration names. 208 | inlinevar-naming-style=any 209 | 210 | # Regular expression matching correct inline iteration names. Overrides 211 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 212 | # with the set naming style. 213 | #inlinevar-rgx= 214 | 215 | # Naming style matching correct method names. 216 | method-naming-style=snake_case 217 | 218 | # Regular expression matching correct method names. Overrides method-naming- 219 | # style. If left empty, method names will be checked with the set naming style. 220 | #method-rgx= 221 | 222 | # Naming style matching correct module names. 223 | module-naming-style=snake_case 224 | 225 | # Regular expression matching correct module names. Overrides module-naming- 226 | # style. If left empty, module names will be checked with the set naming style. 227 | #module-rgx= 228 | 229 | # Colon-delimited sets of names that determine each other's naming style when 230 | # the name regexes allow several styles. 231 | name-group= 232 | 233 | # Regular expression which should only match function or class names that do 234 | # not require a docstring. 235 | no-docstring-rgx=^_ 236 | 237 | # List of decorators that produce properties, such as abc.abstractproperty. Add 238 | # to this list to register other decorators that produce valid properties. 239 | # These decorators are taken in consideration only for invalid-name. 240 | property-classes=abc.abstractproperty 241 | 242 | # Regular expression matching correct type alias names. If left empty, type 243 | # alias names will be checked with the set naming style. 244 | #typealias-rgx= 245 | 246 | # Regular expression matching correct type variable names. If left empty, type 247 | # variable names will be checked with the set naming style. 248 | #typevar-rgx= 249 | 250 | # Naming style matching correct variable names. 251 | variable-naming-style=snake_case 252 | 253 | # Regular expression matching correct variable names. Overrides variable- 254 | # naming-style. If left empty, variable names will be checked with the set 255 | # naming style. 256 | #variable-rgx= 257 | 258 | 259 | [CLASSES] 260 | 261 | # Warn about protected attribute access inside special methods 262 | check-protected-access-in-special-methods=no 263 | 264 | # List of method names used to declare (i.e. assign) instance attributes. 265 | defining-attr-methods=__init__, 266 | __new__, 267 | setUp, 268 | asyncSetUp, 269 | __post_init__ 270 | 271 | # List of member names, which should be excluded from the protected access 272 | # warning. 273 | exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit 274 | 275 | # List of valid names for the first argument in a class method. 276 | valid-classmethod-first-arg=cls 277 | 278 | # List of valid names for the first argument in a metaclass class method. 279 | valid-metaclass-classmethod-first-arg=mcs 280 | 281 | 282 | [DESIGN] 283 | 284 | # List of regular expressions of class ancestor names to ignore when counting 285 | # public methods (see R0903) 286 | exclude-too-few-public-methods= 287 | 288 | # List of qualified class names to ignore when counting class parents (see 289 | # R0901) 290 | ignored-parents= 291 | 292 | # Maximum number of arguments for function / method. 293 | max-args=5 294 | 295 | # Maximum number of attributes for a class (see R0902). 296 | max-attributes=7 297 | 298 | # Maximum number of boolean expressions in an if statement (see R0916). 299 | max-bool-expr=5 300 | 301 | # Maximum number of branch for function / method body. 302 | max-branches=12 303 | 304 | # Maximum number of locals for function / method body. 305 | max-locals=15 306 | 307 | # Maximum number of parents for a class (see R0901). 308 | max-parents=7 309 | 310 | # Maximum number of positional arguments for function / method. 311 | #max-positional-arguments=5 312 | 313 | # Maximum number of public methods for a class (see R0904). 314 | max-public-methods=20 315 | 316 | # Maximum number of return / yield for function / method body. 317 | max-returns=6 318 | 319 | # Maximum number of statements in function / method body. 320 | max-statements=50 321 | 322 | # Minimum number of public methods for a class (see R0903). 323 | min-public-methods=2 324 | 325 | 326 | [EXCEPTIONS] 327 | 328 | # Exceptions that will emit a warning when caught. 329 | overgeneral-exceptions=builtins.BaseException,builtins.Exception 330 | 331 | 332 | [FORMAT] 333 | 334 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 335 | expected-line-ending-format= 336 | 337 | # Regexp for a line that is allowed to be longer than the limit. 338 | ignore-long-lines=^\s*(# )??$ 339 | 340 | # Number of spaces of indent required inside a hanging or continued line. 341 | indent-after-paren=4 342 | 343 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 344 | # tab). 345 | indent-string=' ' 346 | 347 | # Maximum number of characters on a single line. 348 | max-line-length=100 349 | 350 | # Maximum number of lines in a module. 351 | max-module-lines=1000 352 | 353 | # Allow the body of a class to be on the same line as the declaration if body 354 | # contains single statement. 355 | single-line-class-stmt=no 356 | 357 | # Allow the body of an if to be on the same line as the test if there is no 358 | # else. 359 | single-line-if-stmt=no 360 | 361 | 362 | [IMPORTS] 363 | 364 | # List of modules that can be imported at any level, not just the top level 365 | # one. 366 | allow-any-import-level= 367 | 368 | # Allow explicit reexports by alias from a package __init__. 369 | allow-reexport-from-package=no 370 | 371 | # Allow wildcard imports from modules that define __all__. 372 | allow-wildcard-with-all=no 373 | 374 | # Deprecated modules which should not be used, separated by a comma. 375 | deprecated-modules= 376 | 377 | # Output a graph (.gv or any supported image format) of external dependencies 378 | # to the given file (report RP0402 must not be disabled). 379 | ext-import-graph= 380 | 381 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 382 | # external) dependencies to the given file (report RP0402 must not be 383 | # disabled). 384 | import-graph= 385 | 386 | # Output a graph (.gv or any supported image format) of internal dependencies 387 | # to the given file (report RP0402 must not be disabled). 388 | int-import-graph= 389 | 390 | # Force import order to recognize a module as part of the standard 391 | # compatibility libraries. 392 | known-standard-library= 393 | 394 | # Force import order to recognize a module as part of a third party library. 395 | known-third-party=enchant 396 | 397 | # Couples of modules and preferred modules, separated by a comma. 398 | preferred-modules= 399 | 400 | 401 | [LOGGING] 402 | 403 | # The type of string formatting that logging methods do. `old` means using % 404 | # formatting, `new` is for `{}` formatting. 405 | logging-format-style=old 406 | 407 | # Logging modules to check that the string format arguments are in logging 408 | # function parameter format. 409 | logging-modules=logging 410 | 411 | 412 | [MESSAGES CONTROL] 413 | 414 | # Only show warnings with the listed confidence levels. Leave empty to show 415 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 416 | # UNDEFINED. 417 | confidence=HIGH, 418 | CONTROL_FLOW, 419 | INFERENCE, 420 | INFERENCE_FAILURE, 421 | UNDEFINED 422 | 423 | # Disable the message, report, category or checker with the given id(s). You 424 | # can either give multiple identifiers separated by comma (,) or put this 425 | # option multiple times (only on the command line, not in the configuration 426 | # file where it should appear only once). You can also use "--disable=all" to 427 | # disable everything first and then re-enable specific checks. For example, if 428 | # you want to run only the similarities checker, you can use "--disable=all 429 | # --enable=similarities". If you want to run only the classes checker, but have 430 | # no Warning level messages displayed, use "--disable=all --enable=classes 431 | # --disable=W". 432 | disable=raw-checker-failed, 433 | bad-inline-option, 434 | locally-disabled, 435 | file-ignored, 436 | suppressed-message, 437 | useless-suppression, 438 | deprecated-pragma, 439 | use-symbolic-message-instead, 440 | use-implicit-booleaness-not-comparison-to-string, 441 | use-implicit-booleaness-not-comparison-to-zero, 442 | no-else-continue, 443 | no-else-return, 444 | no-else-raise, 445 | no-else-break, 446 | consider-iterating-dictionary 447 | 448 | # Enable the message, report, category or checker with the given id(s). You can 449 | # either give multiple identifier separated by comma (,) or put this option 450 | # multiple time (only on the command line, not in the configuration file where 451 | # it should appear only once). See also the "--disable" option for examples. 452 | enable= 453 | 454 | 455 | [METHOD_ARGS] 456 | 457 | # List of qualified names (i.e., library.method) which require a timeout 458 | # parameter e.g. 'requests.api.get,requests.api.post' 459 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 460 | 461 | 462 | [MISCELLANEOUS] 463 | 464 | # List of note tags to take in consideration, separated by a comma. 465 | notes=FIXME, 466 | XXX, 467 | TODO 468 | 469 | # Regular expression of note tags to take in consideration. 470 | notes-rgx= 471 | 472 | 473 | [REFACTORING] 474 | 475 | # Maximum number of nested blocks for function / method body 476 | max-nested-blocks=5 477 | 478 | # Complete name of functions that never returns. When checking for 479 | # inconsistent-return-statements if a never returning function is called then 480 | # it will be considered as an explicit return statement and no message will be 481 | # printed. 482 | never-returning-functions=sys.exit,argparse.parse_error 483 | 484 | # Let 'consider-using-join' be raised when the separator to join on would be 485 | # non-empty (resulting in expected fixes of the type: ``"- " + " - 486 | # ".join(items)``) 487 | suggest-join-with-non-empty-separator=yes 488 | 489 | 490 | [REPORTS] 491 | 492 | # Python expression which should return a score less than or equal to 10. You 493 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 494 | # 'convention', and 'info' which contain the number of messages in each 495 | # category, as well as 'statement' which is the total number of statements 496 | # analyzed. This score is used by the global evaluation report (RP0004). 497 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 498 | 499 | # Template used to display messages. This is a python new-style format string 500 | # used to format the message information. See doc for all details. 501 | msg-template= 502 | 503 | # Set the output format. Available formats are: text, parseable, colorized, 504 | # json2 (improved json format), json (old json format) and msvs (visual 505 | # studio). You can also give a reporter class, e.g. 506 | # mypackage.mymodule.MyReporterClass. 507 | #output-format= 508 | 509 | # Tells whether to display a full report or only the messages. 510 | reports=no 511 | 512 | # Activate the evaluation score. 513 | score=yes 514 | 515 | 516 | [SIMILARITIES] 517 | 518 | # Comments are removed from the similarity computation 519 | ignore-comments=yes 520 | 521 | # Docstrings are removed from the similarity computation 522 | ignore-docstrings=yes 523 | 524 | # Imports are removed from the similarity computation 525 | ignore-imports=yes 526 | 527 | # Signatures are removed from the similarity computation 528 | ignore-signatures=yes 529 | 530 | # Minimum lines number of a similarity. 531 | min-similarity-lines=4 532 | 533 | 534 | [SPELLING] 535 | 536 | # Limits count of emitted suggestions for spelling mistakes. 537 | max-spelling-suggestions=4 538 | 539 | # Spelling dictionary name. No available dictionaries : You need to install 540 | # both the python package and the system dependency for enchant to work. 541 | spelling-dict= 542 | 543 | # List of comma separated words that should be considered directives if they 544 | # appear at the beginning of a comment and should not be checked. 545 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 546 | 547 | # List of comma separated words that should not be checked. 548 | spelling-ignore-words= 549 | 550 | # A path to a file that contains the private dictionary; one word per line. 551 | spelling-private-dict-file= 552 | 553 | # Tells whether to store unknown words to the private dictionary (see the 554 | # --spelling-private-dict-file option) instead of raising a message. 555 | spelling-store-unknown-words=no 556 | 557 | 558 | [STRING] 559 | 560 | # This flag controls whether inconsistent-quotes generates a warning when the 561 | # character used as a quote delimiter is used inconsistently within a module. 562 | check-quote-consistency=no 563 | 564 | # This flag controls whether the implicit-str-concat should generate a warning 565 | # on implicit string concatenation in sequences defined over several lines. 566 | check-str-concat-over-line-jumps=no 567 | 568 | 569 | [TYPECHECK] 570 | 571 | # List of decorators that produce context managers, such as 572 | # contextlib.contextmanager. Add to this list to register other decorators that 573 | # produce valid context managers. 574 | contextmanager-decorators=contextlib.contextmanager 575 | 576 | # List of members which are set dynamically and missed by pylint inference 577 | # system, and so shouldn't trigger E1101 when accessed. Python regular 578 | # expressions are accepted. 579 | generated-members= 580 | 581 | # Tells whether to warn about missing members when the owner of the attribute 582 | # is inferred to be None. 583 | ignore-none=yes 584 | 585 | # This flag controls whether pylint should warn about no-member and similar 586 | # checks whenever an opaque object is returned when inferring. The inference 587 | # can return multiple potential results while evaluating a Python object, but 588 | # some branches might not be evaluated, which results in partial inference. In 589 | # that case, it might be useful to still emit no-member and other checks for 590 | # the rest of the inferred objects. 591 | ignore-on-opaque-inference=yes 592 | 593 | # List of symbolic message names to ignore for Mixin members. 594 | ignored-checks-for-mixins=no-member, 595 | not-async-context-manager, 596 | not-context-manager, 597 | attribute-defined-outside-init 598 | 599 | # List of class names for which member attributes should not be checked (useful 600 | # for classes with dynamically set attributes). This supports the use of 601 | # qualified names. 602 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 603 | 604 | # Show a hint with possible names when a member name was not found. The aspect 605 | # of finding the hint is based on edit distance. 606 | missing-member-hint=yes 607 | 608 | # The minimum edit distance a name should have in order to be considered a 609 | # similar match for a missing member name. 610 | missing-member-hint-distance=1 611 | 612 | # The total number of similar names that should be taken in consideration when 613 | # showing a hint for a missing member. 614 | missing-member-max-choices=1 615 | 616 | # Regex pattern to define which classes are considered mixins. 617 | mixin-class-rgx=.*[Mm]ixin 618 | 619 | # List of decorators that change the signature of a decorated function. 620 | signature-mutators= 621 | 622 | 623 | [VARIABLES] 624 | 625 | # List of additional names supposed to be defined in builtins. Remember that 626 | # you should avoid defining new builtins when possible. 627 | additional-builtins= 628 | 629 | # Tells whether unused global variables should be treated as a violation. 630 | allow-global-unused-variables=yes 631 | 632 | # List of names allowed to shadow builtins 633 | allowed-redefined-builtins= 634 | 635 | # List of strings which can identify a callback function by name. A callback 636 | # name must start or end with one of those strings. 637 | callbacks=cb_, 638 | _cb 639 | 640 | # A regular expression matching the name of dummy variables (i.e. expected to 641 | # not be used). 642 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 643 | 644 | # Argument names that match this expression will be ignored. 645 | ignored-argument-names=_.*|^ignored_|^unused_ 646 | 647 | # Tells whether we should check for unused import in __init__ files. 648 | init-import=no 649 | 650 | # List of qualified module names which can have objects that can redefine 651 | # builtins. 652 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 653 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | 676 | -------------------------------------------------------------------------------- /simulator/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.10" 3 | 4 | [[package]] 5 | name = "async-timeout" 6 | version = "5.0.1" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, 11 | ] 12 | 13 | [[package]] 14 | name = "brother-printer-fwupd-snmp-simulator" 15 | version = "0.1.0" 16 | source = { virtual = "." } 17 | dependencies = [ 18 | { name = "snmpsim" }, 19 | { name = "zeroconf" }, 20 | ] 21 | 22 | [package.metadata] 23 | requires-dist = [ 24 | { name = "snmpsim", specifier = ">=1.1.7" }, 25 | { name = "zeroconf", specifier = ">=0.136.2" }, 26 | ] 27 | 28 | [[package]] 29 | name = "certifi" 30 | version = "2024.12.14" 31 | source = { registry = "https://pypi.org/simple" } 32 | sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } 33 | wheels = [ 34 | { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, 35 | ] 36 | 37 | [[package]] 38 | name = "cffi" 39 | version = "1.17.1" 40 | source = { registry = "https://pypi.org/simple" } 41 | dependencies = [ 42 | { name = "pycparser" }, 43 | ] 44 | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } 45 | wheels = [ 46 | { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, 47 | { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, 48 | { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, 49 | { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, 50 | { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, 51 | { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, 52 | { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, 53 | { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, 54 | { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, 55 | { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, 56 | { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, 57 | { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, 58 | { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, 59 | { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, 60 | { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, 61 | { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, 62 | { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, 63 | { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, 64 | { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, 65 | { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, 66 | { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, 67 | { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, 68 | { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, 69 | { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, 70 | { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, 71 | { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, 72 | { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, 73 | { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, 74 | { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, 75 | { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, 76 | { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, 77 | { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, 78 | { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, 79 | { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, 80 | { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, 81 | { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, 82 | { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, 83 | { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, 84 | { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, 85 | { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, 86 | { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, 87 | { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, 88 | { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, 89 | { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, 90 | { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, 91 | { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, 92 | ] 93 | 94 | [[package]] 95 | name = "charset-normalizer" 96 | version = "3.4.1" 97 | source = { registry = "https://pypi.org/simple" } 98 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 99 | wheels = [ 100 | { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, 101 | { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, 102 | { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, 103 | { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, 104 | { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, 105 | { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, 106 | { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, 107 | { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, 108 | { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, 109 | { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, 110 | { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, 111 | { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, 112 | { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, 113 | { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, 114 | { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, 115 | { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, 116 | { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, 117 | { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, 118 | { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, 119 | { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, 120 | { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, 121 | { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, 122 | { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, 123 | { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, 124 | { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, 125 | { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, 126 | { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, 127 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, 128 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, 129 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, 130 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, 131 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, 132 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, 133 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, 134 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, 135 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, 136 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, 137 | { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, 138 | { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, 139 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, 140 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, 141 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, 142 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, 143 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, 144 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, 145 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, 146 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, 147 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, 148 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, 149 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, 150 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 151 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 152 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 153 | ] 154 | 155 | [[package]] 156 | name = "cryptography" 157 | version = "44.0.0" 158 | source = { registry = "https://pypi.org/simple" } 159 | dependencies = [ 160 | { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, 161 | ] 162 | sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 } 163 | wheels = [ 164 | { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 }, 165 | { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 }, 166 | { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 }, 167 | { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 }, 168 | { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 }, 169 | { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 }, 170 | { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 }, 171 | { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 }, 172 | { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 }, 173 | { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 }, 174 | { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 }, 175 | { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 }, 176 | { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 }, 177 | { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 }, 178 | { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 }, 179 | { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 }, 180 | { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 }, 181 | { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 }, 182 | { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 }, 183 | { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 }, 184 | { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857 }, 185 | { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615 }, 186 | { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622 }, 187 | { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546 }, 188 | { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937 }, 189 | { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 }, 190 | ] 191 | 192 | [[package]] 193 | name = "idna" 194 | version = "3.10" 195 | source = { registry = "https://pypi.org/simple" } 196 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 197 | wheels = [ 198 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 199 | ] 200 | 201 | [[package]] 202 | name = "ifaddr" 203 | version = "0.2.0" 204 | source = { registry = "https://pypi.org/simple" } 205 | sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485 } 206 | wheels = [ 207 | { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314 }, 208 | ] 209 | 210 | [[package]] 211 | name = "jinja2" 212 | version = "3.1.5" 213 | source = { registry = "https://pypi.org/simple" } 214 | dependencies = [ 215 | { name = "markupsafe" }, 216 | ] 217 | sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } 218 | wheels = [ 219 | { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, 220 | ] 221 | 222 | [[package]] 223 | name = "markupsafe" 224 | version = "3.0.2" 225 | source = { registry = "https://pypi.org/simple" } 226 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } 227 | wheels = [ 228 | { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, 229 | { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, 230 | { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, 231 | { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, 232 | { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, 233 | { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, 234 | { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, 235 | { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, 236 | { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, 237 | { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, 238 | { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, 239 | { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, 240 | { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, 241 | { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, 242 | { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, 243 | { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, 244 | { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, 245 | { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, 246 | { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, 247 | { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, 248 | { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, 249 | { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, 250 | { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, 251 | { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, 252 | { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, 253 | { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, 254 | { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, 255 | { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, 256 | { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, 257 | { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, 258 | { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, 259 | { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, 260 | { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, 261 | { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, 262 | { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, 263 | { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, 264 | { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, 265 | { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, 266 | { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, 267 | { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, 268 | { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, 269 | { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, 270 | { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, 271 | { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, 272 | { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, 273 | { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, 274 | { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, 275 | { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, 276 | { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, 277 | { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, 278 | ] 279 | 280 | [[package]] 281 | name = "ply" 282 | version = "3.11" 283 | source = { registry = "https://pypi.org/simple" } 284 | sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130 } 285 | wheels = [ 286 | { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567 }, 287 | ] 288 | 289 | [[package]] 290 | name = "pyasn1" 291 | version = "0.6.1" 292 | source = { registry = "https://pypi.org/simple" } 293 | sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } 294 | wheels = [ 295 | { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, 296 | ] 297 | 298 | [[package]] 299 | name = "pycparser" 300 | version = "2.22" 301 | source = { registry = "https://pypi.org/simple" } 302 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } 303 | wheels = [ 304 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, 305 | ] 306 | 307 | [[package]] 308 | name = "pysmi" 309 | version = "1.5.9" 310 | source = { registry = "https://pypi.org/simple" } 311 | dependencies = [ 312 | { name = "jinja2" }, 313 | { name = "ply" }, 314 | { name = "requests" }, 315 | ] 316 | sdist = { url = "https://files.pythonhosted.org/packages/be/b0/19085322fc24f6b4a7bbc151643641c391b8b8d9aa4fd0e40d8178b2a5f5/pysmi-1.5.9.tar.gz", hash = "sha256:f6dfda838e3cba133169f1ff57f71a2841815d43db2e5c619b6e5db3b8560707", size = 131529 } 317 | wheels = [ 318 | { url = "https://files.pythonhosted.org/packages/7b/b7/8b3307ce9ba5d0af9a23535daa281f8f9034d6273e10ee36699f0cb64e80/pysmi-1.5.9-py3-none-any.whl", hash = "sha256:3deb22e6341ba4c7d545056745adf091d86c35028d21003638944e67e42b87fa", size = 85453 }, 319 | ] 320 | 321 | [[package]] 322 | name = "pysnmp" 323 | version = "6.2.6" 324 | source = { registry = "https://pypi.org/simple" } 325 | dependencies = [ 326 | { name = "pyasn1" }, 327 | { name = "pysmi" }, 328 | { name = "pysnmpcrypto" }, 329 | ] 330 | sdist = { url = "https://files.pythonhosted.org/packages/bc/a2/1d0f9cb0cff7e45c651be1ce9b646d0cc837697bc030fc9ffb90247303d4/pysnmp-6.2.6.tar.gz", hash = "sha256:ce86b74d0ce4b74ffe9124993f2778c66111fe7523828ebe9bd1b067f9635c00", size = 414288 } 331 | wheels = [ 332 | { url = "https://files.pythonhosted.org/packages/78/bf/4a4ddb36bc21c6331eb1a10e0caa6b75db56e8e46b97e86a28550a078609/pysnmp-6.2.6-py3-none-any.whl", hash = "sha256:d95cc0bc3d1c69eefbf71ab0ecb5d802a2130081b31d67aad5371c3f85ed6465", size = 268252 }, 333 | ] 334 | 335 | [[package]] 336 | name = "pysnmpcrypto" 337 | version = "0.0.4" 338 | source = { registry = "https://pypi.org/simple" } 339 | dependencies = [ 340 | { name = "cryptography" }, 341 | ] 342 | sdist = { url = "https://files.pythonhosted.org/packages/3e/87/86a32362944ea2d554dce797f3988e9a9bdd24447b906c99d44d1f70506a/pysnmpcrypto-0.0.4.tar.gz", hash = "sha256:b635fb3b1ec6637b9a0033f50506214e16eb84574b1d25ab027bbde4caa55129", size = 7244 } 343 | wheels = [ 344 | { url = "https://files.pythonhosted.org/packages/dd/ee/3ba0ddd4650ad5251479be614dacff65db920274f575825d48663a4ce7b9/pysnmpcrypto-0.0.4-py2.py3-none-any.whl", hash = "sha256:5889733caa030f45d9e03ea9d6370fb06426a8cb7f839aabbcdde33c6f634679", size = 6502 }, 345 | ] 346 | 347 | [[package]] 348 | name = "requests" 349 | version = "2.32.3" 350 | source = { registry = "https://pypi.org/simple" } 351 | dependencies = [ 352 | { name = "certifi" }, 353 | { name = "charset-normalizer" }, 354 | { name = "idna" }, 355 | { name = "urllib3" }, 356 | ] 357 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 358 | wheels = [ 359 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 360 | ] 361 | 362 | [[package]] 363 | name = "snmpsim" 364 | version = "1.1.7" 365 | source = { registry = "https://pypi.org/simple" } 366 | dependencies = [ 367 | { name = "pysnmp" }, 368 | ] 369 | sdist = { url = "https://files.pythonhosted.org/packages/0a/e3/cad55059628c554f7d5b2f232307bc21436da2ca246994733f57b6bcbdd0/snmpsim-1.1.7.tar.gz", hash = "sha256:22a0d81a2d2b00ae9f3d7eec1bcf4c12e09e637dd88504d896e841a05cb67ea0", size = 240005 } 370 | wheels = [ 371 | { url = "https://files.pythonhosted.org/packages/0a/3f/24dc63f2867b3636067c3dd110c7e377fa7e822402fa9b22bebb7a5363fa/snmpsim-1.1.7-py3-none-any.whl", hash = "sha256:c5f9dac3066d7debae3eb6dbf030e35bbb2f6a6c5d11feefd9d0a4b46680c575", size = 226001 }, 372 | ] 373 | 374 | [[package]] 375 | name = "urllib3" 376 | version = "2.3.0" 377 | source = { registry = "https://pypi.org/simple" } 378 | sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } 379 | wheels = [ 380 | { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, 381 | ] 382 | 383 | [[package]] 384 | name = "zeroconf" 385 | version = "0.136.2" 386 | source = { registry = "https://pypi.org/simple" } 387 | dependencies = [ 388 | { name = "async-timeout", marker = "python_full_version < '3.11'" }, 389 | { name = "ifaddr" }, 390 | ] 391 | sdist = { url = "https://files.pythonhosted.org/packages/89/e0/cddc7e2272799e11ea172810fd2f6da58fc7290f9bd3102d6cf2c385cca3/zeroconf-0.136.2.tar.gz", hash = "sha256:37d223febad4569f0d14563eb8e80a9742be35d0419847b45d84c37fc4224bb4", size = 238720 } 392 | wheels = [ 393 | { url = "https://files.pythonhosted.org/packages/0c/9d/880d677d6c70127566898000ff33f0e770441dcb07ae652b537f78a6fec2/zeroconf-0.136.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5dbd7a2d9fbce5623b05ecd7003fab95bc324dcb8e33b35ce796e8e915851e4d", size = 1979623 }, 394 | { url = "https://files.pythonhosted.org/packages/31/dd/c1833e233b2070717dad9708591524748002e87e4a1e8d04cacf066ba161/zeroconf-0.136.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b958e163ebbaeb1d2e01dcf62de8c44af566af97a93a7a969e515e522f4d83f", size = 1763840 }, 395 | { url = "https://files.pythonhosted.org/packages/82/c6/1d65332f6e415c0e91b69d5ffbe436039fa379111f4f978880dc72bb8ecc/zeroconf-0.136.2-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:e6de231a0ba53a5b6fa6c2b9b6be8cc9e017404721fdb46316e4e28379ecc216", size = 9913419 }, 396 | { url = "https://files.pythonhosted.org/packages/25/7d/2a6b341831fbf4227a5d64c2ca8a94c55b4756d8395ccced9afaec12711b/zeroconf-0.136.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c8a8d99828c14e45122f856d05bd66d19cd70ba5c9930aaee71f1ec6e28561c", size = 10416088 }, 397 | { url = "https://files.pythonhosted.org/packages/35/01/3fa4c28438cd921b5d2219fe3d6590541bb093797dd0d6d6a70ec0a762b1/zeroconf-0.136.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:90443a6006d2b721001f7db8f1e10e687e52229cff49075fcbcf66036c133c94", size = 10205033 }, 398 | { url = "https://files.pythonhosted.org/packages/84/d8/1735b1f3872cda8259d02d8ccceead3833ff273b2bb2d1959ff698900cf1/zeroconf-0.136.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b999043357fc4caedff042d6bea3cf7c7ca205c976e5c7233fe8d56a5cf346e0", size = 10537433 }, 399 | { url = "https://files.pythonhosted.org/packages/93/16/734b5ee991550705687d51b738c7bc474f4d0b53895f108999f62588a48e/zeroconf-0.136.2-cp310-cp310-win32.whl", hash = "sha256:117087d963883604792bc91118be0bcecde07c1c41189db8f835b1fd3bd27eaa", size = 1423751 }, 400 | { url = "https://files.pythonhosted.org/packages/95/4c/5dae354c51f1620475930b43a55c3d32b1b48182148ea63ff9293e5daa43/zeroconf-0.136.2-cp310-cp310-win_amd64.whl", hash = "sha256:d68d8cff8aa4b6e3b0722d853b9c70873add50503bc40f2b2bddb5856c6f6193", size = 1648543 }, 401 | { url = "https://files.pythonhosted.org/packages/1d/7c/4db8f465f804510dff1cf265d8deb59bc0fabc0797e325f736c0b5458862/zeroconf-0.136.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c35e991091013cbe02907c4b798554c1081630925c8a94dcd652c2440fed8907", size = 1981148 }, 402 | { url = "https://files.pythonhosted.org/packages/e2/bf/2e29a15b42ec0fcd647b22b6c0f98cc8ce6279cac4e85ec0221ef4f3d620/zeroconf-0.136.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1efe072eb34f51c2b5386981ab725a87d2ee31078ce4c5a488669eda10415cb6", size = 1765580 }, 403 | { url = "https://files.pythonhosted.org/packages/8c/2b/27cbf25c0e767bf8a32b76d2189e3973acb9a281451984144bac08fa2046/zeroconf-0.136.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ba59ab362e99dd9063a3321d6bf933b062847257e3248490051a042d61a130", size = 11322979 }, 404 | { url = "https://files.pythonhosted.org/packages/25/b6/25507f4a0ed8f3428d3ddc22c36b78b00901b652f43dd4a0bc79e4367170/zeroconf-0.136.2-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:7ce7ba8462f3baab4cd0332c929fde3dabc7d814a0381f33a3b871d9f7886626", size = 10840220 }, 405 | { url = "https://files.pythonhosted.org/packages/a3/3d/6518ce1ab7d583a5dd410d218cac3144af2d6345492196a821d878f26f52/zeroconf-0.136.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:019205377af6b2308476e6597b8ae34a7cfe6fe3b6e367b0ab405ff17c4d4d3b", size = 11378175 }, 406 | { url = "https://files.pythonhosted.org/packages/fa/b0/5ac541bccda7ec2ea9d9f9d4996ee28bdebff879eb31e4529032f34741fc/zeroconf-0.136.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d5437c28e0898ba018b52e23f6ffa9cefa2560649b50947e2220d6daa1b17db9", size = 11226808 }, 407 | { url = "https://files.pythonhosted.org/packages/88/6e/fcfaa76d160b14bbdd0ce0f51f6e65ef2731219452d6ed4e1eeec8523447/zeroconf-0.136.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c5a6007b0e3b938e5b6af4a12ee23ef997ca89068702aea5fc66baf216fbcbd", size = 11728551 }, 408 | { url = "https://files.pythonhosted.org/packages/25/2c/461e3612cdab1373709afdcbc098ce9e9c0833a346f46e5468b5339c9459/zeroconf-0.136.2-cp311-cp311-win32.whl", hash = "sha256:6afa19d0dcbfc656ecc987f33c313e30178a20d3731d17eb6c81388138954e8c", size = 1420631 }, 409 | { url = "https://files.pythonhosted.org/packages/e8/ee/b650b3b089cb5cab82f0a36b3a74eb363af7fae7d0166d9eb68e4e1582cd/zeroconf-0.136.2-cp311-cp311-win_amd64.whl", hash = "sha256:0ac3f8b375d1e3954908de0d6245df6ca44d01ecbd2d50572a0d86fc18e7b1f2", size = 1651622 }, 410 | { url = "https://files.pythonhosted.org/packages/f2/b0/d1787251efd3c613b4d6be3f38ed01cefe9b02541493e2285e50b4dd8138/zeroconf-0.136.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5a74474e4d5cda8ea121e7b9538660decce1b944a210a26134ca5852c1e12929", size = 1983567 }, 411 | { url = "https://files.pythonhosted.org/packages/a6/0a/00bcafbcbf4a2f385365e6664109fc8016fda5928a88be2e9e5787158c58/zeroconf-0.136.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1746e25128915417d1500e23cdb22f7460bb688978e4570cc956081fc85c408f", size = 1785378 }, 412 | { url = "https://files.pythonhosted.org/packages/cc/65/d7a215dd9c67c3dd6674792af58155e15387b30194bd4258fea25e284d64/zeroconf-0.136.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb14c95c428ad003f0a8fdde87f0c0ba89a945ced468421ad24818e243e3fd0e", size = 11018968 }, 413 | { url = "https://files.pythonhosted.org/packages/65/63/741a4dd5d0e312591a78ae7cc5a32b6e437f7471bdeb4dadbf4dcdfa0c70/zeroconf-0.136.2-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c44256cbc15e4dc8f5466e9f20a86325156b1a5d4bfc704167f93ed0653058b6", size = 10539880 }, 414 | { url = "https://files.pythonhosted.org/packages/0d/f2/0dcf3442b4202fe39057440e07b747052a2f2490b880793e55c8d9f64d8a/zeroconf-0.136.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9286e280d5aec66d0ac9ef662d953a08aba8b812c0e3273b790026b7c143ee98", size = 11176186 }, 415 | { url = "https://files.pythonhosted.org/packages/b4/13/ca435edfdcfc1661029aaaec40d9668ec131bb9c28bfda62470fc3b6268a/zeroconf-0.136.2-cp312-cp312-manylinux_2_36_x86_64.whl", hash = "sha256:1e0f0df103bc27816b00b983cdc404423fd14daaa4708ad7764a9ca3830a333d", size = 11221155 }, 416 | { url = "https://files.pythonhosted.org/packages/f1/31/c6cc9b13287ed30571e503a323813c8993c304e455e3560b666562e67539/zeroconf-0.136.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:21351dca8727ba7c47d00f97de413b353ab2da72044fdae55eb32cf52998f5b6", size = 11024634 }, 417 | { url = "https://files.pythonhosted.org/packages/09/53/9798777405860300485cfea7c0811c439358796613bcf6d7eb9d4e7f7672/zeroconf-0.136.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e908a3c93c15261416397c7acd624ad58b048e8027a048c07e35cb5fe1bab489", size = 11516089 }, 418 | { url = "https://files.pythonhosted.org/packages/15/94/f93da045105d725b31961f8f02e1d61dadb29db797cf2a6806794d6f209e/zeroconf-0.136.2-cp312-cp312-win32.whl", hash = "sha256:0ed19d4e0a2cfe57a19c6540253af6f254a5d79ed17af7dd14db125a9faa95de", size = 1424214 }, 419 | { url = "https://files.pythonhosted.org/packages/58/ad/274fac20c7a94e9f635d6d524b56abd8b114ae8905e8208a8a43f7103e40/zeroconf-0.136.2-cp312-cp312-win_amd64.whl", hash = "sha256:38b05678968dc2cfdef795f14efc16d5cb9163e008da5f15334bccc235f8c722", size = 1649049 }, 420 | { url = "https://files.pythonhosted.org/packages/a1/4e/98f7b35ea904ee1af875000c66411b5468ee2d6641f2ab5a1768917eecce/zeroconf-0.136.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffc9bd9df2c30edfbc44c02413f1191d509ed7c749cd405172344320dc4bac03", size = 1975045 }, 421 | { url = "https://files.pythonhosted.org/packages/c2/67/66ede11511b7e2bd5a3f468857ffae77fff33c0df9ddef71f9021cc8a65a/zeroconf-0.136.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbd29811c60382929fef9fe70dd85d3d0a26efd1a6068ced86b24ab7c15c9649", size = 1764692 }, 422 | { url = "https://files.pythonhosted.org/packages/0d/98/d2843af05145c5b9ee40592f8af1583115be1915de78ae4db3fe49b4a234/zeroconf-0.136.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88cc3744a4b8613765a49837c88fde2ff3f4b41fac0661591c7203391358f644", size = 10923373 }, 423 | { url = "https://files.pythonhosted.org/packages/97/8f/d94d9802a34ab81980cf8cd8b583c5401ec18effecd66f95049e713d4911/zeroconf-0.136.2-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:94b3df37ee57d0b77330aee6cb8a3612363ead8f04c57110751773945532e05e", size = 10447845 }, 424 | { url = "https://files.pythonhosted.org/packages/d1/bc/57b7acf36027116069d8eaed713c57a5af476643170f1bc4aa290c097ce0/zeroconf-0.136.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d009191d7d13ba552d4c0dbb96792c5473c8d31668aa47343e324f0f61b0fbd", size = 11105833 }, 425 | { url = "https://files.pythonhosted.org/packages/8a/8e/d332512795e405dd923547d6b0cb7620370cf63866134be238d2ab7e0b89/zeroconf-0.136.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756d8e5427026d12f42fe452952c19de3545ea16e70af0a999404c7ef196057b", size = 10916033 }, 426 | { url = "https://files.pythonhosted.org/packages/9c/00/96e186c87d7134725b9a67ba03a33fd2f784bb80aff993a494b38b385033/zeroconf-0.136.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e63369e50f6aef945ee036e94d105ff6ec84498f6e7d6e456abac577916d4d2f", size = 11427960 }, 427 | { url = "https://files.pythonhosted.org/packages/31/30/405c29621c9885069bd6ba83819bb2f59835e426ddffb2e2754919678b42/zeroconf-0.136.2-cp313-cp313-win32.whl", hash = "sha256:816f04d03625a876aa6d1b389c64f5421d5210a9bddf69c997ea1bb377970613", size = 1419666 }, 428 | { url = "https://files.pythonhosted.org/packages/3c/23/50b8c743e6cf33d4e2336388afa77cbdaf1cad80513685b7541bc8ed981e/zeroconf-0.136.2-cp313-cp313-win_amd64.whl", hash = "sha256:ce480e2f84f2176c19f04d55bcd2ee540dc7262ad6eb8fc8194d1b98c9f33f2a", size = 1643386 }, 429 | { url = "https://files.pythonhosted.org/packages/76/ea/22f6495444b00b170dd2b0a2cf177b862382ef04a0594d9f717c557f93be/zeroconf-0.136.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:97ee3439c9e10f51cb690923972d213fae5e2d2be63504f2f318255fa7471a6f", size = 1602629 }, 430 | { url = "https://files.pythonhosted.org/packages/c8/39/d4610b305d84a65c7775b8ef05f456dc59083c8202c07fc53edbef4b6e28/zeroconf-0.136.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:346688e5d28b370192d721fcf85a72193f8e7cf8f5e625bf1c111f00e404ca65", size = 1501496 }, 431 | { url = "https://files.pythonhosted.org/packages/2d/c1/9bfbe5bd88c7ae4a6e25fbf351021acefd59e6be43c219c9d3ee4ebf2d0d/zeroconf-0.136.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:1a4d2946ad71b514279b6897911406a4b6f95b95efff6d4c9762f0818068ec9d", size = 1903538 }, 432 | { url = "https://files.pythonhosted.org/packages/fa/1e/8147c5d738945a9c94ddd451538c37edd0cbddb85fa8499952069c802ecf/zeroconf-0.136.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4aaf4088a20800858797a9e03f38d698382ad2f54d2e9133b66dff5bc8a82c5", size = 1859183 }, 433 | { url = "https://files.pythonhosted.org/packages/5f/c2/af89c76f6de7a7eac0963ddf669b7c28fc523c52347b60ef45573960ea6a/zeroconf-0.136.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3e79263b2482cfd39bfe2013c2517dfb76c52fe3f94a697d131cf1322738e61", size = 1537223 }, 434 | ] 435 | --------------------------------------------------------------------------------