Skip to content

Commit 176b999

Browse files
authored
Merge pull request #13 from john-liu2/task/code75
Add `check_latest_version()` and `update_package()` logic
2 parents 3eaa6f2 + 29ae80e commit 176b999

File tree

12 files changed

+286
-36
lines changed

12 files changed

+286
-36
lines changed

.github/workflows/pylint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Install dependencies
1818
run: |
1919
python -m pip install --upgrade pip
20-
pip install pylint ruff click loguru piexif pillow pillow-heif tqdm numpy opencv-python
20+
pip install pylint ruff click loguru piexif pillow pillow-heif tqdm numpy opencv-python packaging requests
2121
- name: Analysing the code with pylint
2222
run: |
2323
pylint $(git ls-files '*.py')

.github/workflows/pytest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Install dependencies
2525
run: |
2626
python -m pip install --upgrade pip
27-
pip install pytest-cov click loguru piexif pillow pillow-heif tqdm numpy opencv-python
27+
pip install pytest-cov click loguru piexif pillow pillow-heif tqdm numpy opencv-python packaging requests
2828
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
2929
- name: Test with pytest
3030
run: |

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ uv pip install --upgrade batch_img
5252
Usage: batch_img [OPTIONS] COMMAND [ARGS]...
5353
5454
Options:
55-
--version Show this tool's version.
55+
--update Update the tool to the latest version.
56+
--version Show the tool's version.
5657
--help Show this message and exit.
5758
5859
Commands:

batch_img/common.py

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,107 @@
22
Copyright © 2025 John Liu
33
"""
44

5+
import importlib.metadata
56
import itertools
67
import json
78
import subprocess
89
import tomllib
910
from datetime import datetime
10-
from importlib.metadata import version
1111
from multiprocessing import Pool, cpu_count
1212
from os.path import getmtime, getsize
1313
from pathlib import Path
1414

1515
import piexif
1616
import pillow_heif
17+
import requests
1718
from loguru import logger
19+
from packaging import version # compare versions safely
1820
from PIL import Image, ImageChops
1921
from PIL.TiffImagePlugin import IFDRational
2022
from tqdm import tqdm
2123

22-
from batch_img.const import PATTERNS, PKG_NAME, REPLACE, TS_FORMAT, VER
24+
from batch_img.const import PATTERNS, REPLACE, TS_FORMAT, VER
2325

2426
pillow_heif.register_heif_opener() # allow Pillow to open HEIC files
2527

2628

2729
class Common:
2830
@staticmethod
29-
def get_version() -> str:
30-
"""
31-
Get this package version using several ways
31+
def get_version(pkg_name: str) -> str:
32+
"""Get this package version by various ways
33+
34+
Args:
35+
pkg_name: package name str
36+
37+
Returns:
38+
str:
3239
"""
3340
try:
34-
return version(PKG_NAME)
41+
return importlib.metadata.version(pkg_name)
3542
except (FileNotFoundError, ImportError, ValueError) as e:
36-
# Use lazy % formatting in logging for efficiency
37-
logger.warning(f"importlib.metadata.version Error: {e}")
38-
logger.debug("Try to get version from pyproject.toml file")
43+
logger.warning(f"importlib.metadata.version() Error: {e}")
44+
logger.debug("Get version from pyproject.toml file")
3945
pyproject = Path(__file__).parent.parent / "pyproject.toml"
4046
with open(pyproject, "rb") as f:
4147
return tomllib.load(f)["project"][VER]
4248

49+
@staticmethod
50+
def check_latest_version(pkg_name: str) -> str:
51+
"""Check if the installed version is the latest one
52+
53+
Args:
54+
pkg_name: package name str
55+
56+
Returns:
57+
str
58+
"""
59+
try:
60+
jsn_url = f"https://pypi.org/pypi/{pkg_name}/json"
61+
response = requests.get(jsn_url, timeout=8)
62+
if response.status_code != 200:
63+
msg = f"⚠️ Error get data from PyPI: {jsn_url}"
64+
logger.error(msg)
65+
return msg
66+
67+
latest_ver = response.json()["info"]["version"]
68+
cur_ver = Common.get_version(pkg_name)
69+
if version.parse(cur_ver) < version.parse(latest_ver):
70+
msg = (
71+
f"🔔 Update available: {cur_ver}{latest_ver}\n"
72+
f"Please run '{pkg_name} --update'"
73+
)
74+
else:
75+
msg = f"✅ {pkg_name} is up to date ({cur_ver})"
76+
logger.info(msg)
77+
except requests.RequestException as e:
78+
msg = f"requests.get() Exception: {e}"
79+
logger.error(msg)
80+
except (KeyError, json.JSONDecodeError) as e:
81+
msg = f"Error parse PyPI response: {e}"
82+
logger.error(msg)
83+
return msg
84+
85+
@staticmethod
86+
def update_package(pkg_name: str) -> str:
87+
"""Update the package to the latest version
88+
89+
Args:
90+
pkg_name: package name str
91+
92+
Returns:
93+
str
94+
"""
95+
logger.info(f"🔄 Updating {pkg_name} ...")
96+
cmd = f"uv pip install --upgrade {pkg_name}"
97+
try:
98+
Common.run_cmd(cmd)
99+
msg = "✅ Update completed."
100+
logger.info(msg)
101+
except subprocess.CalledProcessError as e:
102+
msg = f"❌ Failed to update {pkg_name}: {e}"
103+
logger.error(msg)
104+
return msg
105+
43106
@staticmethod
44107
def run_cmd(cmd: str) -> tuple:
45108
"""Run a command on the host and get the output

batch_img/interface.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
import click
66

77
from batch_img.common import Common
8-
from batch_img.const import MSG_BAD, MSG_OK
8+
from batch_img.const import MSG_BAD, MSG_OK, PKG_NAME
99
from batch_img.main import Main
1010

1111

1212
@click.group(invoke_without_command=True)
1313
@click.pass_context
14-
@click.option("--version", is_flag=True, help="Show this tool's version.")
15-
def cli(ctx, version): # pragma: no cover
14+
@click.option("--update", is_flag=True, help="Update the tool to the latest version.")
15+
@click.option("--version", is_flag=True, help="Show the tool's version.")
16+
def cli(ctx, update, version): # pragma: no cover
1617
if not ctx.invoked_subcommand:
18+
if update:
19+
Common.update_package(PKG_NAME)
1720
if version:
18-
click.secho(Common.get_version())
21+
click.secho(Common.get_version(PKG_NAME))
1922

2023

2124
@cli.command(help="Add internal border to image file(s), not expand the size.")

batch_img/main.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from loguru import logger
1111

1212
from batch_img.border import Border
13+
from batch_img.common import Common
1314
from batch_img.const import PKG_NAME, REPLACE, TS_FORMAT
1415
from batch_img.resize import Resize
1516
from batch_img.rotate import Rotate
@@ -43,15 +44,16 @@ def resize(options: dict) -> bool:
4344
Main.init_log_file()
4445
in_path = Path(options["src_path"])
4546
length = options.get("length")
46-
output = options.get("output")
4747
if not length or length == 0:
48-
logger.warning(f"No resize due to bad {length=}")
48+
logger.error(f"No resize due to bad {length=}")
4949
return False
50+
output = options.get("output")
5051
out = Path(output) if output else REPLACE
5152
if in_path.is_file():
5253
ok, _ = Resize.resize_an_image(in_path, out, length)
5354
else:
5455
ok = Resize.resize_all_progress_bar(in_path, out, length)
56+
Common.check_latest_version(PKG_NAME)
5557
return ok
5658

5759
@staticmethod
@@ -68,15 +70,16 @@ def rotate(options: dict) -> bool:
6870
Main.init_log_file()
6971
in_path = Path(options["src_path"])
7072
angle = options.get("angle")
71-
output = options.get("output")
7273
if not angle or angle == 0:
73-
logger.warning(f"No rotate due to bad {angle=}")
74+
logger.error(f"No rotate due to bad {angle=}")
7475
return False
76+
output = options.get("output")
7577
out = Path(output) if output else REPLACE
7678
if in_path.is_file():
7779
ok, _ = Rotate.rotate_1_image(in_path, out, angle)
7880
else:
7981
ok = Rotate.rotate_all_in_dir(in_path, out, angle)
82+
Common.check_latest_version(PKG_NAME)
8083
return ok
8184

8285
@staticmethod
@@ -93,14 +96,18 @@ def border(options: dict) -> bool:
9396
Main.init_log_file()
9497
in_path = Path(options["src_path"])
9598
bd_width = options.get("border_width")
96-
bd_color = options.get("border_color")
97-
output = options.get("output")
9899
if not bd_width or bd_width == 0:
99-
logger.warning(f"No add border due to bad {bd_width=}")
100+
logger.error(f"Bad border width: {bd_width=}")
101+
return False
102+
bd_color = options.get("border_color")
103+
if not bd_color:
104+
logger.error(f"Bad border color: {bd_color=}")
100105
return False
106+
output = options.get("output")
101107
out = Path(output) if output else REPLACE
102108
if in_path.is_file():
103109
ok, _ = Border.border_1_image(in_path, out, bd_width, bd_color)
104110
else:
105111
ok = Border.border_all_in_dir(in_path, out, bd_width, bd_color)
112+
Common.check_latest_version(PKG_NAME)
106113
return ok

pyproject.toml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ requires = ["setuptools>=80.0"]
44

55
[project]
66
name = "batch_img"
7-
version = "0.0.8"
8-
description = "Batch processing image files by utilizing Pillow / PIL library"
7+
version = "0.0.9"
8+
description = "Batch processing (resize, rotate, add border) image files (HEIC, JPG, PNG)"
99
readme = "README.md"
1010
authors = [{ name = "John Liu" }]
1111
license-files = ["LICENSE"]
@@ -20,12 +20,14 @@ requires-python = ">=3.12"
2020
dependencies = [
2121
"click",
2222
"loguru",
23+
"numpy", # check orientation
24+
"opencv-python", # check orientation by face
25+
"packaging",
2326
"piexif",
2427
"pillow",
2528
"pillow-heif",
29+
"requests",
2630
"tqdm",
27-
"numpy", # check rotate
28-
"opencv-python", # check rotate by face
2931
]
3032
[project.scripts]
3133
batch_img = "batch_img.interface:cli"
@@ -85,8 +87,11 @@ exclude = [
8587
"**/build/",
8688
"**/generated/**/*.py",
8789
]
90+
8891
[tool.pytest.ini_options]
8992
markers = [
9093
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
9194
"serial",
9295
]
96+
[run]
97+
parallel = true

pyproject_release.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ requires = ["setuptools>=80.0"]
44

55
[project]
66
name = "batch_img"
7-
version = "0.0.8"
8-
description = "Batch processing image files by utilizing Pillow / PIL library"
7+
version = "0.0.9"
8+
description = "Batch processing (resize, rotate, add border) image files (HEIC, JPG, PNG)"
99
readme = "README.md"
1010
authors = [{ name = "John Liu" }]
1111
license-files = ["LICENSE"]
@@ -20,12 +20,14 @@ requires-python = ">=3.12"
2020
dependencies = [
2121
"click",
2222
"loguru",
23+
"numpy",
24+
"opencv-python",
25+
"packaging",
2326
"piexif",
2427
"pillow",
2528
"pillow-heif",
29+
"requests",
2630
"tqdm",
27-
"numpy",
28-
"opencv-python",
2931
]
3032
[project.scripts]
3133
batch_img = "batch_img.interface:cli"

tests/cmd.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
curl --header 'Accept: application/json' -v "https://pypistats.org/api/packages/batch-img/recent?period=week" > ~/Downloads/stats.json
2+
3+
curl --header 'Accept: application/json' -v "https://pypi.org/pypi/batch-img/json" > ~/Downloads/versions.json

tests/test_common.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,60 @@
99
from unittest.mock import MagicMock, patch
1010

1111
import pytest
12+
import requests
1213

1314
from batch_img.common import Common
1415
from batch_img.const import NAME, PKG_NAME, VER
1516
from .helper import DotDict
1617

1718

18-
@pytest.fixture(params=[({NAME: PKG_NAME, VER: "0.1.2"}, "0.0.8"), ({}, "0.0.8")])
19+
@pytest.fixture(params=[(PKG_NAME, "0.0.9"), ("", "0.0.9")])
1920
def ver_data(request):
2021
return request.param
2122

2223

2324
def test_get_version(ver_data):
24-
fake_v, expected = ver_data
25-
mock = MagicMock()
26-
mock.version = expected
27-
actual = Common.get_version()
25+
pkg_name, expected = ver_data
26+
actual = Common.get_version(pkg_name)
2827
assert actual == expected
2928

3029

30+
@pytest.fixture(
31+
params=[
32+
(PKG_NAME, f"✅ {PKG_NAME} is up to date (0.0.9)"),
33+
(
34+
"bad_bogus",
35+
f"⚠️ Error get data from PyPI: https://pypi.org/pypi/bad_bogus/json",
36+
),
37+
]
38+
)
39+
def data_check_latest_version(request):
40+
return request.param
41+
42+
43+
def test_check_latest_version(data_check_latest_version):
44+
pkg_name, expected = data_check_latest_version
45+
actual = Common.check_latest_version(pkg_name)
46+
assert actual == expected
47+
48+
49+
@patch("requests.get")
50+
def test_error1_check_latest_version(mock_req_get):
51+
mock_req_get.side_effect = requests.RequestException
52+
actual = Common.check_latest_version(PKG_NAME)
53+
assert actual == "requests.get() Exception: "
54+
55+
56+
@patch("requests.get")
57+
def test_error2_check_latest_version(mock_req_get):
58+
mock_response = MagicMock()
59+
mock_response.status_code = 200
60+
mock_response.json.return_value = {"key": "value"}
61+
mock_req_get.return_value = mock_response
62+
actual = Common.check_latest_version(PKG_NAME)
63+
assert actual == "Error parse PyPI response: 'info'"
64+
65+
3166
@pytest.fixture(
3267
params=[
3368
({}, "cmd1 -op1 | grep k1", (0, "", "")),

0 commit comments

Comments
 (0)