Skip to content

Commit

Permalink
Improve project (switch to uv, add unit tests, update dependencies, f…
Browse files Browse the repository at this point in the history
…ix linter errors, add more entrypoint scripts, ...)
  • Loading branch information
sedrubal committed Jan 5, 2025
1 parent 0d62f13 commit 8918195
Show file tree
Hide file tree
Showing 32 changed files with 3,420 additions and 1,830 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
dist/
__pycache__
.pyre/
.tox/
43 changes: 29 additions & 14 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,44 @@
---

repos:
- repo: https://github.com/ambv/black
rev: 24.2.0
# - repo: https://github.com/psf/black
# rev: 24.10.0
# hooks:
# - id: black
# language_version: python3

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.6
hooks:
- id: black
language_version: python3
- id: ruff
# Run the linter.
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
# Run the formatter.

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.14.1
hooks:
- id: mypy
additional_dependencies:
- types-requests>=2.31.0
- types-termcolor>=1.1.6

- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: '^.*\.md$'
exclude: '^.*\.(md|snmprec)$'
# - id: debug-statements
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
# - id: flake8
- id: mixed-line-ending
args: ['--fix=lf']
args: ["--fix=lf"]
exclude: '^.*\.bat$'

# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: 'v0.910'
# hooks:
# - id: mypy

...
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,21 @@ pip install --user --upgrade brother-printer-fwupd

1. Clone the repo
2. Install system dependencies: `libxslt-dev`, `libxml2-dev`
3. `poetry install --extras autodiscover`
4. `poetry run brother_printer_fwupd`
3. `uv sync --all-extras`
4. `uv run brother-printer-fwupd --help`
5. `uv run brother-printer-fwupd-autodiscover --help`
6. `uv run brother-printer-fwupd-snmp-info --help`

Use at your own risk!™

Contributions welcome.

## Unit tests

```bash
uv run tox
```

## License

[© 2024 sedrubal (GPLv3)](./LICENSE)
[© 2025 sedrubal (GPLv3)](./LICENSE)
10 changes: 10 additions & 0 deletions brother_printer_fwupd/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
"""Tool to update the firmware of some Brother printers (e. g. MFC)."""

import importlib.metadata as importlib_metadata

try:
__version__ = importlib_metadata.version(__name__)
except importlib_metadata.PackageNotFoundError:
__version__ = "unknown"


ISSUE_URL = "https://github.com/sedrubal/brother_printer_fwupd/issues/new"
204 changes: 58 additions & 146 deletions brother_printer_fwupd/__main__.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,130 +1,32 @@
#!/usr/bin/env python3
"""
Script to update the firmware of some Brother printers (e. g. MFC).
"""

import argparse
import logging
import sys
import typing
import webbrowser
from pathlib import Path
from typing import TYPE_CHECKING, Optional

import termcolor

from . import ISSUE_URL
from .common import common_args, printer_info_args
from .firmware_downloader import fw_downloader_args, get_info_and_download_fw
from .firmware_uploader import fw_uploader_args, upload_fw
from .models import SNMPPrinterInfo
from .snmp_info import get_snmp_info_sync, snmp_args
from .utils import CONSOLE_LOG_HANDLER, LOGGER, GitHubIssueReporter

try:
from .autodiscover_printer import PrinterDiscoverer
except ImportError:
PrinterDiscoverer = None

from .firmware_downloader import download_fw, get_download_url
from .firmware_uploader import upload_fw
from .models import FWInfo, SNMPPrinterInfo
from .snmp_info import get_snmp_info
from .utils import (
CONSOLE_LOG_HANDLER,
LOGGER,
GitHubIssueReporter,
get_running_os,
gooey_if_exists,
)

if TYPE_CHECKING:
from .models import IPAddress


RUNNING_OS = get_running_os()

from .autodiscovery import PrinterDiscoverer

@gooey_if_exists
def parse_args():
"""Parse command line args."""
parser = argparse.ArgumentParser(
prog=__file__,
description=__doc__.strip().splitlines()[0],
)
if PrinterDiscoverer:
blurb = "default: autodiscover via mdns"
else:
blurb = "required, because zeroconf is not available"
parser.add_argument(
"-p",
"--printer",
required=not PrinterDiscoverer,
dest="printer",
metavar="host",
default=None,
help=f"IP Address or hostname of the printer ({blurb})).",
)
parser.add_argument(
"--model",
dest="model",
type=str,
help="Skip SNMP scanning by directly specifying the printer model.",
)
parser.add_argument(
"--serial",
dest="serial",
type=str,
help="Skip SNMP scanning by directly specifying the printer serial.",
)
parser.add_argument(
"--spec",
dest="spec",
type=str,
help="Skip SNMP scanning by directly specifying the printer spec.",
)
parser.add_argument(
"--fw",
"--fw-versions",
dest="fw_versions",
nargs="*",
default=[], # In Python 3.10+: list[FWInfo]
type=FWInfo.from_str,
help="Skip SNMP scanning by directly specifying the firmware parts to update.",
)
parser.add_argument(
"-c",
"--community",
dest="community",
default="public",
help="SNMP Community string for the printer (default: '%(default)s').",
)
parser.add_argument(
"-o",
"--fw-dir",
type=Path,
dest="fw_dir",
default=".",
help="Directory, where the firmware will be downloaded (default: '%(default)s').",
)
parser.add_argument(
"--os",
dest="os",
type=str.upper,
default=RUNNING_OS,
choices=["WINDOWS", "MAC", "LINUX"],
help="Operating system to report when downloading firmware (default: '%(default)s').",
)
parser.add_argument(
"--download-only",
dest="download_only",
action="store_true",
help=(
"Do no install update but download firmware and save it"
" under the directory path given with --fw-dir."
),
)
parser.add_argument(
"--debug",
dest="debug",
action="store_true",
help="Print debug messages",
)
PRINTER_DISCOVERER: typing.Optional[typing.Type[PrinterDiscoverer]] = PrinterDiscoverer
except ImportError:
PRINTER_DISCOVERER = None

return parser.parse_args()
if typing.TYPE_CHECKING:
from .models import IPAddress


def main():
Expand Down Expand Up @@ -176,21 +78,21 @@ def run(issue_reporter: GitHubIssueReporter):

CONSOLE_LOG_HANDLER.setLevel(logging.DEBUG if args.debug else logging.INFO)

printer_ip: Optional["IPAddress"] = args.printer
upload_port: Optional[int] = None
use_snmp = (
not args.model or not args.serial or not args.spec or not args.fw_versions
)
printer_ip: "typing.Optional[IPAddress]" = args.ip
upload_port: typing.Optional[int] = args.pdl_ds_port
use_snmp = not args.model or not args.serial or not args.spec or not args.fw_versions
printer_ip_required = use_snmp or not args.download_only

if not printer_ip and printer_ip_required:
LOGGER.info("Discovering printer via MDNS.")
discoverer = PrinterDiscoverer()
assert PRINTER_DISCOVERER
discoverer = PRINTER_DISCOVERER()
mdns_printer_info = discoverer.run_cli()

if mdns_printer_info:
printer_ip = mdns_printer_info.ip_addr
upload_port = mdns_printer_info.port
if not upload_port:
upload_port = mdns_printer_info.port

if not printer_ip and printer_ip_required:
LOGGER.critical("No printer given or found.")
Expand All @@ -202,14 +104,11 @@ def run(issue_reporter: GitHubIssueReporter):
if use_snmp:
LOGGER.info("Querying printer info via SNMP.")
assert printer_ip, "Printer IP is required but not given."
printer_info = get_snmp_info(target=printer_ip, community=args.community)
else:
printer_info = SNMPPrinterInfo(
model=args.model,
serial=args.serial,
spec=args.spec,
fw_versions=args.fw_versions,
printer_info: SNMPPrinterInfo = get_snmp_info_sync(
target=printer_ip, community=args.community, port=args.snmp_port
)
else:
printer_info = SNMPPrinterInfo.from_args(args)

if printer_info.model:
issue_reporter.set_context_data("--model", printer_info.model)
Expand All @@ -234,40 +133,27 @@ def run(issue_reporter: GitHubIssueReporter):
versions_str,
)
LOGGER.info("Querying firmware download URL from Brother update API.")
download_url: Optional[str] = None

for fw_part in printer_info.fw_versions:
LOGGER.info("Try to get information for firmware part %s", fw_part)

latest_version, download_url = get_download_url(
fw_file_path = get_info_and_download_fw(
printer_info=printer_info,
firmid=str(fw_part.firmid),
reported_os=args.os,
fw_part=fw_part,
os=args.os,
fw_dir=args.fw_dir,
)

if not download_url:
if not fw_file_path:
continue

assert download_url

LOGGER.debug(" Download URL is %s", download_url)
LOGGER.success("Downloading firmware file.")
fw_file_path = download_fw(
url=download_url,
dst_dir=args.fw_dir,
printer_model=printer_info.model,
fw_part=fw_part,
latest_version=latest_version,
)

if args.download_only:
LOGGER.info("Skipping firmware upload due to --download-only")
else:
LOGGER.info("Uploading firmware file to printer via jetdirect.")
assert printer_ip, "Printer IP is required but not given"
try:
upload_fw(
target=printer_ip, port=upload_port, fw_file_path=fw_file_path
target=printer_ip,
port=args.pdl_ds_port,
fw_file_path=fw_file_path,
)
except OSError as err:
LOGGER.error(
Expand All @@ -276,12 +162,38 @@ def run(issue_reporter: GitHubIssueReporter):
fw_part,
str(err),
)

continue

input("Continue? ")

LOGGER.success("Done.")


def parse_args() -> argparse.Namespace:
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description=__doc__.strip().splitlines()[0],
)

common_args(parser, ip_required=not PRINTER_DISCOVERER)
snmp_args(parser)
printer_info_args(parser)
fw_downloader_args(parser)
fw_uploader_args(parser, set_pdl_ds_port_default=not PRINTER_DISCOVERER)

parser.add_argument(
"--download-only",
dest="download_only",
action="store_true",
help=(
"Do no install update but download firmware and save it"
" under the directory path given with --fw-dir."
),
)

return parser.parse_args()


if __name__ == "__main__":
main()
Loading

0 comments on commit 8918195

Please sign in to comment.